diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..56d5ee3 --- /dev/null +++ b/docs/404.html @@ -0,0 +1 @@ +
'+(n?a:s(a,!0))+"\n
\n":""+(n?a:s(a,!0))+"\n
"},i.prototype.blockquote=function(a){return"\n"+a+"\n"},i.prototype.html=function(a){return a},i.prototype.heading=function(a,e,n){return"
"+a+"
\n"},i.prototype.table=function(a,e){return""+a+"
"},i.prototype.br=function(){return this.options.xhtml?""+s(R.message+"",!0)+"";throw R}}f.exec=f,m.options=m.setOptions=function(a){return h(m.defaults,a),m},m.defaults={gfm:!0,tables:!0,breaks:!1,pedantic:!1,sanitize:!1,sanitizer:null,mangle:!0,smartLists:!1,silent:!1,highlight:null,langPrefix:"lang-",smartypants:!1,headerPrefix:"",renderer:new i,xhtml:!1,baseUrl:null},m.Parser=g,m.parser=g.parse,m.Renderer=i,m.TextRenderer=o,m.Lexer=d,m.lexer=d.lex,m.InlineLexer=r,m.inlineLexer=r.output,m.parse=m,a.exports=m}(this||"undefined"!=typeof window&&window)}).call(this,n("pCvA"))},B0RY:function(a,e,n){var d=n("1MfW")(4696,4800);d.addRange(4608,4680).addRange(4682,4685).addRange(4688,4694).addRange(4698,4701).addRange(4704,4744).addRange(4746,4749).addRange(4752,4784).addRange(4786,4789).addRange(4792,4798).addRange(4802,4805).addRange(4808,4822).addRange(4824,4880).addRange(4882,4885).addRange(4888,4954).addRange(4957,4988).addRange(4992,5017).addRange(11648,11670).addRange(11680,11686).addRange(11688,11694).addRange(11696,11702).addRange(11704,11710).addRange(11712,11718).addRange(11720,11726).addRange(11728,11734).addRange(11736,11742).addRange(43777,43782).addRange(43785,43790).addRange(43793,43798).addRange(43808,43814).addRange(43816,43822),a.exports=d},"B4/L":function(a,e,n){var d=n("gwRl"),t=n("LBQr"),r=n("tb+2"),i=n("E7Xw"),o=Object.getOwnPropertySymbols?function(a){for(var e=[];a;)d(e,r(a)),a=t(a);return e}:i;a.exports=o},B4Jh:function(a,e,n){var d=n("fRAL"),t=Math.max;a.exports=function(a,e,n){return e=t(void 0===e?a.length-1:e,0),function(){for(var r=arguments,i=-1,o=t(r.length-e,0),g=Array(o);++i
' + (escaped ? code : escape(code, true)) + '\\n
';\n }\n\n return '' + (escaped ? code : escape(code, true)) + '\\n
\\n';\n };\n\n Renderer.prototype.blockquote = function (quote) {\n return '\\n' + quote + '\\n';\n };\n\n Renderer.prototype.html = function (html) {\n return html;\n };\n\n Renderer.prototype.heading = function (text, level, raw) {\n return '
' + text + '
\\n';\n };\n\n Renderer.prototype.table = function (header, body) {\n return '' + text + '
';\n };\n\n Renderer.prototype.br = function () {\n return this.options.xhtml ? 'An error occurred:
' + escape(e.message + '', true) + '';\n }\n\n throw e;\n }\n }\n /**\n * Options\n */\n\n\n marked.options = marked.setOptions = function (opt) {\n merge(marked.defaults, opt);\n return marked;\n };\n\n marked.defaults = {\n gfm: true,\n tables: true,\n breaks: false,\n pedantic: false,\n sanitize: false,\n sanitizer: null,\n mangle: true,\n smartLists: false,\n silent: false,\n highlight: null,\n langPrefix: 'lang-',\n smartypants: false,\n headerPrefix: '',\n renderer: new Renderer(),\n xhtml: false,\n baseUrl: null\n };\n /**\n * Expose\n */\n\n marked.Parser = Parser;\n marked.parser = Parser.parse;\n marked.Renderer = Renderer;\n marked.TextRenderer = TextRenderer;\n marked.Lexer = Lexer;\n marked.lexer = Lexer.lex;\n marked.InlineLexer = InlineLexer;\n marked.inlineLexer = InlineLexer.output;\n marked.parse = marked;\n\n if (typeof module !== 'undefined' && typeof exports === 'object') {\n module.exports = marked;\n } else if (typeof define === 'function' && define.amd) {\n define(function () {\n return marked;\n });\n } else {\n root.marked = marked;\n }\n})(this || (typeof window !== 'undefined' ? window : global));","var set = require('regenerate')(0x1258, 0x12C0);\n\nset.addRange(0x1200, 0x1248).addRange(0x124A, 0x124D).addRange(0x1250, 0x1256).addRange(0x125A, 0x125D).addRange(0x1260, 0x1288).addRange(0x128A, 0x128D).addRange(0x1290, 0x12B0).addRange(0x12B2, 0x12B5).addRange(0x12B8, 0x12BE).addRange(0x12C2, 0x12C5).addRange(0x12C8, 0x12D6).addRange(0x12D8, 0x1310).addRange(0x1312, 0x1315).addRange(0x1318, 0x135A).addRange(0x135D, 0x137C).addRange(0x1380, 0x1399).addRange(0x2D80, 0x2D96).addRange(0x2DA0, 0x2DA6).addRange(0x2DA8, 0x2DAE).addRange(0x2DB0, 0x2DB6).addRange(0x2DB8, 0x2DBE).addRange(0x2DC0, 0x2DC6).addRange(0x2DC8, 0x2DCE).addRange(0x2DD0, 0x2DD6).addRange(0x2DD8, 0x2DDE).addRange(0xAB01, 0xAB06).addRange(0xAB09, 0xAB0E).addRange(0xAB11, 0xAB16).addRange(0xAB20, 0xAB26).addRange(0xAB28, 0xAB2E);\nmodule.exports = set;","var arrayPush = require('./_arrayPush'),\n getPrototype = require('./_getPrototype'),\n getSymbols = require('./_getSymbols'),\n stubArray = require('./stubArray');\n\n/* Built-in method references for those with the same name as other `lodash` methods. */\nvar nativeGetSymbols = Object.getOwnPropertySymbols;\n\n/**\n * Creates an array of the own and inherited enumerable symbols of `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @returns {Array} Returns the array of symbols.\n */\nvar getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) {\n var result = [];\n while (object) {\n arrayPush(result, getSymbols(object));\n object = getPrototype(object);\n }\n return result;\n};\n\nmodule.exports = getSymbolsIn;\n","var apply = require('./_apply');\n\n/* Built-in method references for those with the same name as other `lodash` methods. */\nvar nativeMax = Math.max;\n\n/**\n * A specialized version of `baseRest` which transforms the rest array.\n *\n * @private\n * @param {Function} func The function to apply a rest parameter to.\n * @param {number} [start=func.length-1] The start position of the rest parameter.\n * @param {Function} transform The rest array transform.\n * @returns {Function} Returns the new function.\n */\nfunction overRest(func, start, transform) {\n start = nativeMax(start === undefined ? (func.length - 1) : start, 0);\n return function() {\n var args = arguments,\n index = -1,\n length = nativeMax(args.length - start, 0),\n array = Array(length);\n\n while (++index < length) {\n array[index] = args[start + index];\n }\n index = -1;\n var otherArgs = Array(start + 1);\n while (++index < start) {\n otherArgs[index] = args[index];\n }\n otherArgs[start] = transform(array);\n return apply(func, this, otherArgs);\n };\n}\n\nmodule.exports = overRest;\n","/**\n * This function is like `arrayIncludes` except that it accepts a comparator.\n *\n * @private\n * @param {Array} [array] The array to inspect.\n * @param {*} target The value to search for.\n * @param {Function} comparator The comparator invoked per element.\n * @returns {boolean} Returns `true` if `target` is found, else `false`.\n */\nfunction arrayIncludesWith(array, value, comparator) {\n var index = -1,\n length = array == null ? 0 : array.length;\n\n while (++index < length) {\n if (comparator(value, array[index])) {\n return true;\n }\n }\n return false;\n}\n\nmodule.exports = arrayIncludesWith;\n","var set = require('regenerate')();\n\nset.addRange(0x10880, 0x1089E).addRange(0x108A7, 0x108AF);\nmodule.exports = set;","var set = require('regenerate')(0x1039F);\n\nset.addRange(0x10380, 0x1039D);\nmodule.exports = set;","/** Used to match wrap detail comments. */\nvar reWrapComment = /\\{(?:\\n\\/\\* \\[wrapped with .+\\] \\*\\/)?\\n?/;\n\n/**\n * Inserts wrapper `details` in a comment at the top of the `source` body.\n *\n * @private\n * @param {string} source The source to modify.\n * @returns {Array} details The details to insert.\n * @returns {string} Returns the modified source.\n */\nfunction insertWrapDetails(source, details) {\n var length = details.length;\n if (!length) {\n return source;\n }\n var lastIndex = length - 1;\n details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex];\n details = details.join(length > 2 ? ', ' : ' ');\n return source.replace(reWrapComment, '{\\n/* [wrapped with ' + details + '] */\\n');\n}\n\nmodule.exports = insertWrapDetails;\n","var set = require('regenerate')();\n\nset.addRange(0x16B00, 0x16B45).addRange(0x16B50, 0x16B59).addRange(0x16B5B, 0x16B61).addRange(0x16B63, 0x16B77).addRange(0x16B7D, 0x16B8F);\nmodule.exports = set;","var toInteger = require('./_to-integer');\nvar max = Math.max;\nvar min = Math.min;\nmodule.exports = function (index, length) {\n index = toInteger(index);\n return index < 0 ? max(index + length, 0) : min(index, length);\n};\n","var castPath = require('./_castPath'),\n isArguments = require('./isArguments'),\n isArray = require('./isArray'),\n isIndex = require('./_isIndex'),\n isLength = require('./isLength'),\n toKey = require('./_toKey');\n\n/**\n * Checks if `path` exists on `object`.\n *\n * @private\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @param {Function} hasFunc The function to check properties.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n */\nfunction hasPath(object, path, hasFunc) {\n path = castPath(path, object);\n\n var index = -1,\n length = path.length,\n result = false;\n\n while (++index < length) {\n var key = toKey(path[index]);\n if (!(result = object != null && hasFunc(object, key))) {\n break;\n }\n object = object[key];\n }\n if (result || ++index != length) {\n return result;\n }\n length = object == null ? 0 : object.length;\n return !!length && isLength(length) && isIndex(key, length) &&\n (isArray(object) || isArguments(object));\n}\n\nmodule.exports = hasPath;\n","var set = require('regenerate')();\n\nset.addRange(0x2800, 0x28FF);\nmodule.exports = set;","var set = require('regenerate')();\n\nset.addRange(0x10450, 0x1047F);\nmodule.exports = set;","var baseIsMap = require('./_baseIsMap'),\n baseUnary = require('./_baseUnary'),\n nodeUtil = require('./_nodeUtil');\n\n/* Node.js helper references. */\nvar nodeIsMap = nodeUtil && nodeUtil.isMap;\n\n/**\n * Checks if `value` is classified as a `Map` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a map, else `false`.\n * @example\n *\n * _.isMap(new Map);\n * // => true\n *\n * _.isMap(new WeakMap);\n * // => false\n */\nvar isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;\n\nmodule.exports = isMap;\n","/**\n * Gets the value at `key`, unless `key` is \"__proto__\" or \"constructor\".\n *\n * @private\n * @param {Object} object The object to query.\n * @param {string} key The key of the property to get.\n * @returns {*} Returns the property value.\n */\nfunction safeGet(object, key) {\n if (key === 'constructor' && typeof object[key] === 'function') {\n return;\n }\n\n if (key == '__proto__') {\n return;\n }\n\n return object[key];\n}\n\nmodule.exports = safeGet;\n","module.exports = function (it) {\n return typeof it === 'object' ? it !== null : typeof it === 'function';\n};\n","require('../../modules/es6.object.assign');\nmodule.exports = require('../../modules/_core').Object.assign;\n","var isArray = require('./isArray'),\n isSymbol = require('./isSymbol');\n\n/** Used to match property names within property paths. */\nvar reIsDeepProp = /\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/,\n reIsPlainProp = /^\\w*$/;\n\n/**\n * Checks if `value` is a property name and not a property path.\n *\n * @private\n * @param {*} value The value to check.\n * @param {Object} [object] The object to query keys on.\n * @returns {boolean} Returns `true` if `value` is a property name, else `false`.\n */\nfunction isKey(value, object) {\n if (isArray(value)) {\n return false;\n }\n var type = typeof value;\n if (type == 'number' || type == 'symbol' || type == 'boolean' ||\n value == null || isSymbol(value)) {\n return true;\n }\n return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||\n (object != null && value in Object(object));\n}\n\nmodule.exports = isKey;\n","var set = require('regenerate')();\n\nset.addRange(0x10B80, 0x10B91).addRange(0x10B99, 0x10B9C).addRange(0x10BA9, 0x10BAF);\nmodule.exports = set;","var set = require('regenerate')(0x3030, 0x30FB, 0x32FF);\n\nset.addRange(0x2E80, 0x2E99).addRange(0x2E9B, 0x2EF3).addRange(0x2F00, 0x2FD5).addRange(0x3001, 0x3003).addRange(0x3005, 0x3011).addRange(0x3013, 0x301F).addRange(0x3021, 0x302D).addRange(0x3037, 0x303F).addRange(0x3190, 0x319F).addRange(0x31C0, 0x31E3).addRange(0x3220, 0x3247).addRange(0x3280, 0x32B0).addRange(0x32C0, 0x32CB).addRange(0x3358, 0x3370).addRange(0x337B, 0x337F).addRange(0x33E0, 0x33FE).addRange(0x3400, 0x4DBF).addRange(0x4E00, 0x9FFC).addRange(0xA700, 0xA707).addRange(0xF900, 0xFA6D).addRange(0xFA70, 0xFAD9).addRange(0xFE45, 0xFE46).addRange(0xFF61, 0xFF65).addRange(0x16FF0, 0x16FF1).addRange(0x1D360, 0x1D371).addRange(0x1F250, 0x1F251).addRange(0x20000, 0x2A6DD).addRange(0x2A700, 0x2B734).addRange(0x2B740, 0x2B81D).addRange(0x2B820, 0x2CEA1).addRange(0x2CEB0, 0x2EBE0).addRange(0x2F800, 0x2FA1D).addRange(0x30000, 0x3134A);\nmodule.exports = set;","var set = require('regenerate')();\n\nset.addRange(0x11480, 0x114C7).addRange(0x114D0, 0x114D9);\nmodule.exports = set;","var copyObject = require('./_copyObject'),\n getSymbolsIn = require('./_getSymbolsIn');\n\n/**\n * Copies own and inherited symbols of `source` to `object`.\n *\n * @private\n * @param {Object} source The object to copy symbols from.\n * @param {Object} [object={}] The object to copy symbols to.\n * @returns {Object} Returns `object`.\n */\nfunction copySymbolsIn(source, object) {\n return copyObject(source, getSymbolsIn(source), object);\n}\n\nmodule.exports = copySymbolsIn;\n","var getTag = require('./_getTag'),\n isObjectLike = require('./isObjectLike');\n\n/** `Object#toString` result references. */\nvar mapTag = '[object Map]';\n\n/**\n * The base implementation of `_.isMap` without Node.js optimizations.\n *\n * @private\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a map, else `false`.\n */\nfunction baseIsMap(value) {\n return isObjectLike(value) && getTag(value) == mapTag;\n}\n\nmodule.exports = baseIsMap;\n","var set = require('regenerate')();\n\nset.addRange(0x13A0, 0x13F5).addRange(0x13F8, 0x13FD).addRange(0xAB70, 0xABBF);\nmodule.exports = set;","var baseIsEqualDeep = require('./_baseIsEqualDeep'),\n isObjectLike = require('./isObjectLike');\n\n/**\n * The base implementation of `_.isEqual` which supports partial comparisons\n * and tracks traversed objects.\n *\n * @private\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @param {boolean} bitmask The bitmask flags.\n * 1 - Unordered comparison\n * 2 - Partial comparison\n * @param {Function} [customizer] The function to customize comparisons.\n * @param {Object} [stack] Tracks traversed `value` and `other` objects.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n */\nfunction baseIsEqual(value, other, bitmask, customizer, stack) {\n if (value === other) {\n return true;\n }\n if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {\n return value !== value && other !== other;\n }\n return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);\n}\n\nmodule.exports = baseIsEqual;\n","var set = require('regenerate')();\n\nset.addRange(0x1C50, 0x1C7F);\nmodule.exports = set;","var baseIsSet = require('./_baseIsSet'),\n baseUnary = require('./_baseUnary'),\n nodeUtil = require('./_nodeUtil');\n\n/* Node.js helper references. */\nvar nodeIsSet = nodeUtil && nodeUtil.isSet;\n\n/**\n * Checks if `value` is classified as a `Set` object.\n *\n * @static\n * @memberOf _\n * @since 4.3.0\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a set, else `false`.\n * @example\n *\n * _.isSet(new Set);\n * // => true\n *\n * _.isSet(new WeakSet);\n * // => false\n */\nvar isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;\n\nmodule.exports = isSet;\n","'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _extends = Object.assign || function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n};\n\nvar _createClass = function () {\n function defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n }\n\n return function (Constructor, protoProps, staticProps) {\n if (protoProps) defineProperties(Constructor.prototype, protoProps);\n if (staticProps) defineProperties(Constructor, staticProps);\n return Constructor;\n };\n}();\n\nvar _react = require('react');\n\nvar React = _interopRequireWildcard(_react);\n\nfunction _interopRequireWildcard(obj) {\n if (obj && obj.__esModule) {\n return obj;\n } else {\n var newObj = {};\n\n if (obj != null) {\n for (var key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n return newObj;\n }\n}\n\nfunction _objectWithoutProperties(obj, keys) {\n var target = {};\n\n for (var i in obj) {\n if (keys.indexOf(i) >= 0) continue;\n if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;\n target[i] = obj[i];\n }\n\n return target;\n}\n\nfunction _classCallCheck(instance, Constructor) {\n if (!(instance instanceof Constructor)) {\n throw new TypeError(\"Cannot call a class as a function\");\n }\n}\n\nfunction _possibleConstructorReturn(self, call) {\n if (!self) {\n throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n }\n\n return call && (typeof call === \"object\" || typeof call === \"function\") ? call : self;\n}\n\nfunction _inherits(subClass, superClass) {\n if (typeof superClass !== \"function\" && superClass !== null) {\n throw new TypeError(\"Super expression must either be null or a function, not \" + typeof superClass);\n }\n\n subClass.prototype = Object.create(superClass && superClass.prototype, {\n constructor: {\n value: subClass,\n enumerable: false,\n writable: true,\n configurable: true\n }\n });\n if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;\n}\n/* global global */\n\n\nvar KEYCODE_ENTER = 13;\nvar KEYCODE_TAB = 9;\nvar KEYCODE_BACKSPACE = 8;\nvar KEYCODE_Y = 89;\nvar KEYCODE_Z = 90;\nvar KEYCODE_M = 77;\nvar KEYCODE_PARENS = 57;\nvar KEYCODE_BRACKETS = 219;\nvar KEYCODE_QUOTE = 222;\nvar KEYCODE_BACK_QUOTE = 192;\nvar KEYCODE_ESCAPE = 27;\nvar HISTORY_LIMIT = 100;\nvar HISTORY_TIME_GAP = 3000;\nvar isWindows = 'navigator' in global && /Win/i.test(navigator.platform);\nvar isMacLike = 'navigator' in global && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);\nvar className = 'npm__react-simple-code-editor__textarea';\nvar cssText =\n/* CSS */\n'\\n/**\\n * Reset the text fill color so that placeholder is visible\\n */\\n.' + className + ':empty {\\n -webkit-text-fill-color: inherit !important;\\n}\\n\\n/**\\n * Hack to apply on some CSS on IE10 and IE11\\n */\\n@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {\\n /**\\n * IE doesn\\'t support \\'-webkit-text-fill-color\\'\\n * So we use \\'color: transparent\\' to make the text transparent on IE\\n * Unlike other browsers, it doesn\\'t affect caret color in IE\\n */\\n .' + className + ' {\\n color: transparent !important;\\n }\\n\\n .' + className + '::selection {\\n background-color: #accef7 !important;\\n color: transparent !important;\\n }\\n}\\n';\n\nvar Editor = function (_React$Component) {\n _inherits(Editor, _React$Component);\n\n function Editor() {\n var _ref;\n\n var _temp, _this, _ret;\n\n _classCallCheck(this, Editor);\n\n for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Editor.__proto__ || Object.getPrototypeOf(Editor)).call.apply(_ref, [this].concat(args))), _this), _this.state = {\n capture: true\n }, _this._recordCurrentState = function () {\n var input = _this._input;\n if (!input) return; // Save current state of the input\n\n var value = input.value,\n selectionStart = input.selectionStart,\n selectionEnd = input.selectionEnd;\n\n _this._recordChange({\n value: value,\n selectionStart: selectionStart,\n selectionEnd: selectionEnd\n });\n }, _this._getLines = function (text, position) {\n return text.substring(0, position).split('\\n');\n }, _this._recordChange = function (record) {\n var overwrite = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n var _this$_history = _this._history,\n stack = _this$_history.stack,\n offset = _this$_history.offset;\n\n if (stack.length && offset > -1) {\n // When something updates, drop the redo operations\n _this._history.stack = stack.slice(0, offset + 1); // Limit the number of operations to 100\n\n var count = _this._history.stack.length;\n\n if (count > HISTORY_LIMIT) {\n var extras = count - HISTORY_LIMIT;\n _this._history.stack = stack.slice(extras, count);\n _this._history.offset = Math.max(_this._history.offset - extras, 0);\n }\n }\n\n var timestamp = Date.now();\n\n if (overwrite) {\n var last = _this._history.stack[_this._history.offset];\n\n if (last && timestamp - last.timestamp < HISTORY_TIME_GAP) {\n // A previous entry exists and was in short interval\n // Match the last word in the line\n var re = /[^a-z0-9]([a-z0-9]+)$/i; // Get the previous line\n\n var previous = _this._getLines(last.value, last.selectionStart).pop().match(re); // Get the current line\n\n\n var current = _this._getLines(record.value, record.selectionStart).pop().match(re);\n\n if (previous && current && current[1].startsWith(previous[1])) {\n // The last word of the previous line and current line match\n // Overwrite previous entry so that undo will remove whole word\n _this._history.stack[_this._history.offset] = _extends({}, record, {\n timestamp: timestamp\n });\n return;\n }\n }\n } // Add the new operation to the stack\n\n\n _this._history.stack.push(_extends({}, record, {\n timestamp: timestamp\n }));\n\n _this._history.offset++;\n }, _this._updateInput = function (record) {\n var input = _this._input;\n if (!input) return; // Update values and selection state\n\n input.value = record.value;\n input.selectionStart = record.selectionStart;\n input.selectionEnd = record.selectionEnd;\n\n _this.props.onValueChange(record.value);\n }, _this._applyEdits = function (record) {\n // Save last selection state\n var input = _this._input;\n var last = _this._history.stack[_this._history.offset];\n\n if (last && input) {\n _this._history.stack[_this._history.offset] = _extends({}, last, {\n selectionStart: input.selectionStart,\n selectionEnd: input.selectionEnd\n });\n } // Save the changes\n\n\n _this._recordChange(record);\n\n _this._updateInput(record);\n }, _this._undoEdit = function () {\n var _this$_history2 = _this._history,\n stack = _this$_history2.stack,\n offset = _this$_history2.offset; // Get the previous edit\n\n var record = stack[offset - 1];\n\n if (record) {\n // Apply the changes and update the offset\n _this._updateInput(record);\n\n _this._history.offset = Math.max(offset - 1, 0);\n }\n }, _this._redoEdit = function () {\n var _this$_history3 = _this._history,\n stack = _this$_history3.stack,\n offset = _this$_history3.offset; // Get the next edit\n\n var record = stack[offset + 1];\n\n if (record) {\n // Apply the changes and update the offset\n _this._updateInput(record);\n\n _this._history.offset = Math.min(offset + 1, stack.length - 1);\n }\n }, _this._handleKeyDown = function (e) {\n var _this$props = _this.props,\n tabSize = _this$props.tabSize,\n insertSpaces = _this$props.insertSpaces,\n ignoreTabKey = _this$props.ignoreTabKey,\n onKeyDown = _this$props.onKeyDown;\n\n if (onKeyDown) {\n onKeyDown(e);\n\n if (e.defaultPrevented) {\n return;\n }\n }\n\n if (e.keyCode === KEYCODE_ESCAPE) {\n e.target.blur();\n }\n\n var _e$target = e.target,\n value = _e$target.value,\n selectionStart = _e$target.selectionStart,\n selectionEnd = _e$target.selectionEnd;\n var tabCharacter = (insertSpaces ? ' ' : '\\t').repeat(tabSize);\n\n if (e.keyCode === KEYCODE_TAB && !ignoreTabKey && _this.state.capture) {\n // Prevent focus change\n e.preventDefault();\n\n if (e.shiftKey) {\n // Unindent selected lines\n var linesBeforeCaret = _this._getLines(value, selectionStart);\n\n var startLine = linesBeforeCaret.length - 1;\n var endLine = _this._getLines(value, selectionEnd).length - 1;\n var nextValue = value.split('\\n').map(function (line, i) {\n if (i >= startLine && i <= endLine && line.startsWith(tabCharacter)) {\n return line.substring(tabCharacter.length);\n }\n\n return line;\n }).join('\\n');\n\n if (value !== nextValue) {\n var startLineText = linesBeforeCaret[startLine];\n\n _this._applyEdits({\n value: nextValue,\n // Move the start cursor if first line in selection was modified\n // It was modified only if it started with a tab\n selectionStart: startLineText.startsWith(tabCharacter) ? selectionStart - tabCharacter.length : selectionStart,\n // Move the end cursor by total number of characters removed\n selectionEnd: selectionEnd - (value.length - nextValue.length)\n });\n }\n } else if (selectionStart !== selectionEnd) {\n // Indent selected lines\n var _linesBeforeCaret = _this._getLines(value, selectionStart);\n\n var _startLine = _linesBeforeCaret.length - 1;\n\n var _endLine = _this._getLines(value, selectionEnd).length - 1;\n\n var _startLineText = _linesBeforeCaret[_startLine];\n\n _this._applyEdits({\n value: value.split('\\n').map(function (line, i) {\n if (i >= _startLine && i <= _endLine) {\n return tabCharacter + line;\n }\n\n return line;\n }).join('\\n'),\n // Move the start cursor by number of characters added in first line of selection\n // Don't move it if it there was no text before cursor\n selectionStart: /\\S/.test(_startLineText) ? selectionStart + tabCharacter.length : selectionStart,\n // Move the end cursor by total number of characters added\n selectionEnd: selectionEnd + tabCharacter.length * (_endLine - _startLine + 1)\n });\n } else {\n var updatedSelection = selectionStart + tabCharacter.length;\n\n _this._applyEdits({\n // Insert tab character at caret\n value: value.substring(0, selectionStart) + tabCharacter + value.substring(selectionEnd),\n // Update caret position\n selectionStart: updatedSelection,\n selectionEnd: updatedSelection\n });\n }\n } else if (e.keyCode === KEYCODE_BACKSPACE) {\n var hasSelection = selectionStart !== selectionEnd;\n var textBeforeCaret = value.substring(0, selectionStart);\n\n if (textBeforeCaret.endsWith(tabCharacter) && !hasSelection) {\n // Prevent default delete behaviour\n e.preventDefault();\n\n var _updatedSelection = selectionStart - tabCharacter.length;\n\n _this._applyEdits({\n // Remove tab character at caret\n value: value.substring(0, selectionStart - tabCharacter.length) + value.substring(selectionEnd),\n // Update caret position\n selectionStart: _updatedSelection,\n selectionEnd: _updatedSelection\n });\n }\n } else if (e.keyCode === KEYCODE_ENTER) {\n // Ignore selections\n if (selectionStart === selectionEnd) {\n // Get the current line\n var line = _this._getLines(value, selectionStart).pop();\n\n var matches = line.match(/^\\s+/);\n\n if (matches && matches[0]) {\n e.preventDefault(); // Preserve indentation on inserting a new line\n\n var indent = '\\n' + matches[0];\n\n var _updatedSelection2 = selectionStart + indent.length;\n\n _this._applyEdits({\n // Insert indentation character at caret\n value: value.substring(0, selectionStart) + indent + value.substring(selectionEnd),\n // Update caret position\n selectionStart: _updatedSelection2,\n selectionEnd: _updatedSelection2\n });\n }\n }\n } else if (e.keyCode === KEYCODE_PARENS || e.keyCode === KEYCODE_BRACKETS || e.keyCode === KEYCODE_QUOTE || e.keyCode === KEYCODE_BACK_QUOTE) {\n var chars = void 0;\n\n if (e.keyCode === KEYCODE_PARENS && e.shiftKey) {\n chars = ['(', ')'];\n } else if (e.keyCode === KEYCODE_BRACKETS) {\n if (e.shiftKey) {\n chars = ['{', '}'];\n } else {\n chars = ['[', ']'];\n }\n } else if (e.keyCode === KEYCODE_QUOTE) {\n if (e.shiftKey) {\n chars = ['\"', '\"'];\n } else {\n chars = [\"'\", \"'\"];\n }\n } else if (e.keyCode === KEYCODE_BACK_QUOTE && !e.shiftKey) {\n chars = ['`', '`'];\n } // If text is selected, wrap them in the characters\n\n\n if (selectionStart !== selectionEnd && chars) {\n e.preventDefault();\n\n _this._applyEdits({\n value: value.substring(0, selectionStart) + chars[0] + value.substring(selectionStart, selectionEnd) + chars[1] + value.substring(selectionEnd),\n // Update caret position\n selectionStart: selectionStart,\n selectionEnd: selectionEnd + 2\n });\n }\n } else if ((isMacLike ? // Trigger undo with ⌘+Z on Mac\n e.metaKey && e.keyCode === KEYCODE_Z : // Trigger undo with Ctrl+Z on other platforms\n e.ctrlKey && e.keyCode === KEYCODE_Z) && !e.shiftKey && !e.altKey) {\n e.preventDefault();\n\n _this._undoEdit();\n } else if ((isMacLike ? // Trigger redo with ⌘+Shift+Z on Mac\n e.metaKey && e.keyCode === KEYCODE_Z && e.shiftKey : isWindows ? // Trigger redo with Ctrl+Y on Windows\n e.ctrlKey && e.keyCode === KEYCODE_Y : // Trigger redo with Ctrl+Shift+Z on other platforms\n e.ctrlKey && e.keyCode === KEYCODE_Z && e.shiftKey) && !e.altKey) {\n e.preventDefault();\n\n _this._redoEdit();\n } else if (e.keyCode === KEYCODE_M && e.ctrlKey && (isMacLike ? e.shiftKey : true)) {\n e.preventDefault(); // Toggle capturing tab key so users can focus away\n\n _this.setState(function (state) {\n return {\n capture: !state.capture\n };\n });\n }\n }, _this._handleChange = function (e) {\n var _e$target2 = e.target,\n value = _e$target2.value,\n selectionStart = _e$target2.selectionStart,\n selectionEnd = _e$target2.selectionEnd;\n\n _this._recordChange({\n value: value,\n selectionStart: selectionStart,\n selectionEnd: selectionEnd\n }, true);\n\n _this.props.onValueChange(value);\n }, _this._history = {\n stack: [],\n offset: -1\n }, _temp), _possibleConstructorReturn(_this, _ret);\n }\n\n _createClass(Editor, [{\n key: 'componentDidMount',\n value: function componentDidMount() {\n this._recordCurrentState();\n }\n }, {\n key: 'render',\n value: function render() {\n var _this2 = this;\n\n var _props = this.props,\n value = _props.value,\n style = _props.style,\n padding = _props.padding,\n highlight = _props.highlight,\n textareaId = _props.textareaId,\n autoFocus = _props.autoFocus,\n disabled = _props.disabled,\n form = _props.form,\n maxLength = _props.maxLength,\n minLength = _props.minLength,\n name = _props.name,\n placeholder = _props.placeholder,\n readOnly = _props.readOnly,\n required = _props.required,\n onClick = _props.onClick,\n onFocus = _props.onFocus,\n onBlur = _props.onBlur,\n onKeyUp = _props.onKeyUp,\n onKeyDown = _props.onKeyDown,\n onValueChange = _props.onValueChange,\n tabSize = _props.tabSize,\n insertSpaces = _props.insertSpaces,\n ignoreTabKey = _props.ignoreTabKey,\n rest = _objectWithoutProperties(_props, ['value', 'style', 'padding', 'highlight', 'textareaId', 'autoFocus', 'disabled', 'form', 'maxLength', 'minLength', 'name', 'placeholder', 'readOnly', 'required', 'onClick', 'onFocus', 'onBlur', 'onKeyUp', 'onKeyDown', 'onValueChange', 'tabSize', 'insertSpaces', 'ignoreTabKey']);\n\n var contentStyle = {\n paddingTop: padding,\n paddingRight: padding,\n paddingBottom: padding,\n paddingLeft: padding\n };\n var highlighted = highlight(value);\n return React.createElement('div', _extends({}, rest, {\n style: _extends({}, styles.container, style)\n }), React.createElement('textarea', {\n ref: function ref(c) {\n return _this2._input = c;\n },\n style: _extends({}, styles.editor, styles.textarea, contentStyle),\n className: className,\n id: textareaId,\n value: value,\n onChange: this._handleChange,\n onKeyDown: this._handleKeyDown,\n onClick: onClick,\n onKeyUp: onKeyUp,\n onFocus: onFocus,\n onBlur: onBlur,\n disabled: disabled,\n form: form,\n maxLength: maxLength,\n minLength: minLength,\n name: name,\n placeholder: placeholder,\n readOnly: readOnly,\n required: required,\n autoFocus: autoFocus,\n autoCapitalize: 'off',\n autoComplete: 'off',\n autoCorrect: 'off',\n spellCheck: false,\n 'data-gramm': false\n }), React.createElement('pre', _extends({\n 'aria-hidden': 'true',\n style: _extends({}, styles.editor, styles.highlight, contentStyle)\n }, typeof highlighted === 'string' ? {\n dangerouslySetInnerHTML: {\n __html: highlighted + '
{`Researcher: `}\n {`Chris Tsang`}\n {` | Published: 2020-12-24`}
\n{`Impression is a family of algorithms for image simplification, segmentation and vectorization. Try the `}\n {`Demo Web App`}\n {`.`}
\n \n{`The fundamental information result from visual perception is shape. For example, we would describe ‘there is an orange on a basket’ or more conceptually ‘there is a circle above a triangle’.`}
\n{`If we regard simplification as a process of information reduction, we could imagine information being taken away from an image shape by shape, starting from the least important shapes. At the same time, we can also simplify each shape in the image to make it ‘less fractal’.`}
\n{`By controlling this reduction process, we would be able to control the amount of visual information in an image in a quantitative manner.`}
\n{`Under this understanding, segmentation is an extreme degree of simplification, while vectorization is a slight degree of simplification.`}
\n{`The `}\n {`clustering algorithm`}\n {` is hierarchical in nature and the idea is best described by the following verse:`}
\n\n\n\n{`On an unclaimed land, there are villages. Villages ally with each other to form tribes. Tribes conquer each other and form Kingdoms. Kingdoms clashes with each other and form empires. Empires breakdown and finally the entire earth becomes one union.`}
\n\n
{`Clustering is a process to connect pixels of similar colors into clusters. Each cluster have a unique position and shape. There is well known algorithm to perform clustering and Impression’s implementation is essentially same as Kopelman Algorithm.`}
\n{`Hierarchical clustering is a process to build an image tree from the clusters of stage 1. Impression builds a binary tree bottom up. In preparation, clusters are sorted by their size. Starting from the smallest, in each iteration, a cluster is merged into the closest cluster. There are different formulas for ‘close’, but it generally involves comparing the average colors between neighboring clusters.`}
\n{`In image vectorization as in `}\n {`VTracer`}\n {`, we would walk the tree from top to bottom, trace each layer, and stack the vector paths on the output canvas. As if a painter paints on an empty canvas, he would lay down background first and then overlay objects atop, and finally add on details.`}
\n{`In image simplification, there are multiple dimensions of information which can be controlled quantitatively:`}
\n{`Impression currently chose the Ramer-Douglas-Peucker algorithm for shape simplification. While the original algorithm is designed for open curves, Impression adapted it for closed shapes. We cut a closed path into 4 sections, simplify each, and stitch them back together after path simplification. We choose the north most, east most, south most and west most points to cut a given path. Effectively the simplest possible shape is a diamond with four points. However in practice, it is not desirable to have shapes with sharp corners, and so we would smooth them with a 4-point scheme as described in `}\n {`VTracer`}\n {`.`}
\n \n{`It has to be understood that statistically there are exponentially more nodes as we decrease the cluster size. There is one root node known as the image background, but as many leaf nodes as there are number of pixels. As such, fidelity is actually a cutoff which we discard all clusters smaller than a desired size. When this cutoff threshold is low, we are removing salt and pepper noise but in a true color sense. As we increase this cutoff, structures are being discarded. Further increasing this cutoff, the image would become more abstract, and in the end only one solid background will be left.`}
\n{`It is important to note that this background color is not the mean pixel color of the image, but is the average color of the largest cluster, which conceptually is the ‘base tone’ of the given image.`}
\n \n{`More color levels mean finer gradient. Color levels set to 256 means utilizing the full color precision of RGB888. Setting color levels to 32 meaning a cluster has to have at least a color difference of 8 in order to be considered a separate cluster with the cluster on the upper level. In effect, tuning down the color levels would create a retro 8-bit color look. While the number of colors in the palette is limited, the colors are still 24-bit and thus are faithful to the original image.`}
\n \n{`Segmentation`}\n {` follows stage 3 of the algorithm. Similar clusters are grouped together by the `}\n {`disjoint sets`}\n {` algorithm, where each set of the result represents a solid color patch. For each cluster, its neighbours are being considered. If the color difference is smaller than a defined deviation, they will be put into the same union. To prevent grouping too greedily, after the first union, subsequent union would require stricter thresholds. To output segmentation result, each set would be rendered by the average color of all its constituting clusters.`}
\n{`After this stage, there would still be unwanted patches in the output. The output would further be re-clustered by Stage 1 to 3 described before, and an `}\n {`aggregation`}\n {` pass be applied afterwards. It would prune away smaller patches by merging into the closest (in terms of color) cluster if deviation would allow.`}
\n{`Finally, to output result, each aggregate would be rendered by its original cluster color.`}
\n \n{`The described algorithms are implemented as reusable components under `}\n {`VisionMagic`}\n {`. Together with a processing pipeline, they are designed to support real-time and interactive applications. Users can easily organize processing stages and add additional processing passes as needed.`}
\n{`The `}\n
{`The above methodology is designed to imitate visual perception in humans. It is thus no surprising that resulting images exhibit characteristics similar to paintings drawn by Artists. As the name suggests, it is heavy inspired by a painting technique that we called ‘Impressionism’ for the meaning it convey during a brief period of art history.`}
\n{`Simplification Parameters: Shape Details = 27983, Fidelity = 51256, Color Levels = 16\nPhotoshop ‘Oil Paint’ filter: Stylization = 0.1, Cleanliness = 10.0, Scale = 0.1, Bristle Detail = 6.5, Light = -60, Shine = 1.0\n`}
\n \n {`Building photo by `}\n {`Hernan Lucio`}\n {` on `}\n {`Unsplash`}
\n \n \n{`Dog photo by `}\n {`Elijah Ekdahl`}\n {` on `}\n {`Unsplash`}
\n \n \n{`Cityscape photo by `}\n {`Mark Denton`}\n {` on `}\n {`Unsplash`}
\n \n \n{`Landscape photo by `}\n {`Luca Bravo`}\n {` on `}\n {`Unsplash`}
\n \n \n{`Parrot photo by `}\n {`Sanved Bangale`}\n {` on `}\n {`Unsplash`}
\n \n \n{`Water lily photo by `}\n {`Ravi Sharma`}\n {` on `}\n {`Unsplash`}
\n \n\n{`The goal of Semantic Computer Vision is to allow computers to understand the content of images and graphics as intended and perceived by humans, and construct a high level representation of such information.`}
\n{`This technology can be embodied in different applications:`}
\n{`We developed `}\n {`SymCode`}\n {`, a 2D barcode designed to be both human-readable and machine-readable.`}
\n{`We developed `}\n {`Impression`}\n {`, a family of algorithms for image simplification and segmentation. It allows us to control the amount of visual information in an image in a quantitative manner.`}
\n{`We developed `}\n {`VTracer`}\n {`, a utility to convert raster images (like jpg & png) into vector graphics (svg). Our graphics pipeline is able to process high resolution scans and photographs and trace the content to output compact vector files.`}
\n{`We are developing a new Optical Character Recognition (OCR) engine from the ground up specifically for pictorial languages like Chinese, Korean and Japanese (CKJ) to encompass wider character sets and font variations.`}
\n\n{`Researcher: `}\n {`Chris Tsang`}\n {` | Published: 2021-04-01`}
\n{`All we know about the barcode that somehow resembles a Reversi game.`}
\n \n\n\n\n{`Photo by GoToVan via `}\n {`Wikimedia Commons`}
\n\n
{`Disclaimer: I have never been to an Amazon Go store myself. All samples are collected from the internet, and I merely compiled my observations as below. I never received any hints from anyone related to Amazon, so my design and analysis of the Reversi Code is 'clean room'.`}
\n{`057\n⚫🔷⚪⚫⚫⚫\n⚪⚪⚫⚫⚫⚫\n⚫⚪⚫⚫🔷⚫\n⚪🔷⚫⚪⚪⚫\n`}
\n {`206\n⚫⚪⚫⚪⚫⚪\n⚫⚫⚪🔷⚪⚫\n⚫⚫⚪⚫⚪⚫\n🔷⚫⚪⚪⚫🔷\n`}
\n {`268\n⚫⚫⚫⚫⚫⚫\n🔷🔷⚪⚫🔷⚫\n⚫⚪⚫⚪⚪⚫\n⚪⚫⚪⚪⚫⚫\n`}
\n {`322\n⚫🔷⚪⚪⚫⚫\n⚪⚪⚫⚪⚫⚫\n⚫⚪🔷⚪⚫🔷\n⚪⚫⚫⚪⚫⚫\n`}
\n {`508\n⚫⚪⚫⚪⚫⚪\n⚫⚫⚫⚫⚫⚫\n⚫⚫🔷🔷⚪⚪\n⚫🔷⚪⚪⚫⚫\n`}
\n {`520\n⚫⚫⚫⚪⚫⚫\n⚪⚫⚫⚪⚪⚫\n⚫⚫⚫🔷⚫⚫\n⚪🔷⚪⚫⚪🔷\n`}
\n {`628\n⚫⚫⚪⚫⚫⚫\n⚪⚪⚫⚫⚪⚫\n🔷⚪⚫⚪⚫🔷\n⚪⚫🔷⚫⚫⚫\n`}
\n {`974\n🔷⚫⚫⚫🔷⚫\n⚪⚪⚪⚫⚫⚫\n⚫⚪⚫⚫⚪⚫\n⚪⚫⚪⚪⚪🔷\n`}
\n {`906\n🔷⚫⚫⚪⚫⚫\n⚪⚫⚪⚪⚫⚫\n⚫⚫⚪⚪⚪⚫\n⚪🔷⚫⚫⚪🔷\n`}
\n {`445\n🔷⚫⚪⚪⚫⚫\n⚫⚪⚪⚪⚫⚫\n⚫⚫⚪⚪⚫⚫\n⚪⚫🔷⚫⚪🔷\n`}
\n {`Observations:`}
\n{`The matrix is 6x4.`}
\n\n{`There are exactly three diamonds, which can be placed anywhere on the matrix.`}
\n\n{`15 to 17 of them are occupied by circles or diamonds, meaning there are 12 to 14 circles.`}
\n\n{`If we number the modules from top left to bottom right, as in`}
\n\n\n{`0 1 2 3 4 5\n6 7 8 9 10 11\n12 13 14 15 16 17\n18 19 20 21 22 23\n`}
\n\n\n {`The 0th, 4th, 11th, 12th, 19th and 23rd module are always occupied. My guess is they act as a timing pattern.`}
\n\n{`Either 5th or 18th module is occupied, but never both, my guess is they act as an orientation marker (otherwise the code will be rotational symmetric with itself).`}
\n\n{`The remaining 16 positions can freely toggle. Due to #3, 8 to 10 of them must be occupied.`}
\n\n{`There are (16 choose 8) + (16 choose 9) + (16 choose 10) = 32318 combinations.`}
\n\n{`The diamonds can be put on any occupied position, so it multiplies an extra (17 choose 3) + (16 choose 3) + (15 choose 3) = 1695 combinations.`}
\n\n{`It results in 54779010 combinations, just above 2^25 = 33554432\nWhich is reasonable enough, as it is a 6x4 matrix with a little extra freedom.`}
\n\n{`If we set aside 5 bits as redundancy, then the Reversi Code can encode 20 bits payload. This is a reasonable guess in the sense that 20% redundancy provides good error detection capability.`}
\n\n{`This is merely a combinatorial analysis representing the theoretic capacity of the barcode. Even given answers of the payload, it is extremely hard to reverse engineer the complete encoding scheme.`}
\n\n{`There is a decimal number above every barcode on each label. From the layout of the label, we believe that it ranges from 000 to 999. It may or may not directly corresponds to the numeric value of the barcode.`}
\n\n{`Now, we can engineer a barcode that looks exactly the same, but encode data in a different way.`}
\n\n{`Researcher: `}\n {`Sanford Pun`}\n {` | Supervisor: `}\n {`Chris Tsang`}\n {` | Published: 2022-01-02`}
\n{`Repository: `}\n {`visioncortex/ShapeSense`}
\n{`This project aims at recovering (completing) the `}\n {`structure`}\n {` of a shape after a portion of it is erased. In other words, given a shape with arbitrarily \"discontinued parts\", assuming that we have `}\n {`no prior knowledge`}\n {` about the shape, how can we `}\n {`reasonably connect the endpoints`}\n {` at the discontinuities?`}
\n \n{`In the demo images shown throughout this documentation, the following coloring scheme is used: Black pixels denote the background. Red pixels denote the rasterized `}\n {`shape`}\n {`. White pixels denote the `}\n {`hole`}\n {`. The (imaginary) intersection of the shape and the hole is the aforementioned \"discontinued parts\", which is what we try to recover.`}
\n \n{`Blue pixels will be used to denote the outline of the recovered parts of the shape.`}
\n{`The whole process of shape completion involves intrapolating the missing outline and then filling the pixels in the hole with appropriate colors.`}
\n{`Let's begin the experiment with simple shapes, like an ellipse.`}
\n \n{`The first stage of the pipeline is to obtain and process the paths (curves) representing the existing outline of the shape.`}
\n{`Vision Cortex's core library`}\n {` provides the necessary utilities to extract raw paths from an image.`}
\n \n{`The next step is to (divide, if needed, and) extract the two curves from the two endpoints; smoothing is performed to better approximate the tangents near the endpoints (`}\n {`tails`}\n {` of the whole curve). After this step, we will obtain two tangents (2-D direction vectors), one at each tail. We will call these tangents `}\n {`tail tangents`}\n {`.`}
\n \n{`A number of factors determine the accuracy and robustness of tail tangent approximation. Our implementation supports configurations like how many points to consider from the tails, how long should the segments being considered accumulate to, and how the weights for each segment should change towards the tails.`}
\n\n\n\n{`Why \"`}\n {`Intra`}\n {`polation\"?`}\n
\n\n
\n {`\nIf we considered the existing outline of the shape as separate curves at each endpoint, we would be doing `}\n {`inter`}\n {`polation`}\n {` `}\n {`between`}\n {` curves. However, in this project, we are focusing on curves (existing + missing) that form an outline of a single shape, so we argue that we are doing `}\n {`intra`}\n {`polation`}\n {` `}\n {`within`}\n {` a curve.`}
{`With the two endpoints and their corresponding tail tangents, we can calculate for the missing part in different scenarios. The type of curves used in this project is cubic `}\n {`Bézier curves`}\n {`. To specify such a curve, four points are required. An important property to note about this type of curves is that `}\n {`the curve constructed is always fully contained in the quadrilateral defined by the four points`}\n {`.`}
\n{`The first scenario is when the two tail tangents point to the same side with respect to the line connecting the two endpoints (we call this line the `}\n {`base`}\n {`).`}
\n \n{`To construct the curve between A and B, we need to identify two `}\n {`control points`}\n {` (C`}\n {`A`}\n {` and C`}\n {`B`}\n {`) between them. In our approach, we started with what we think is intuitive and made tweaks to resolve some issues in practice; this is what we end up with:`}
\n{`First, find the intersection of the two lines starting at A and B along the corresponding tail tangent. The mid-points between A/B and the intersection are then set to be C`}\n {`A`}\n {` and C`}\n {`B`}\n {`. If the intersection is too far away (i.e. the two lines are close to, if not exactly, parallel), we simply use a point on each line as the control points (e.g. translate A/B along their tail tangent by a factor of base length). Either way, if either C`}\n {`A`}\n {` or C`}\n {`B`}\n {` end up lying outside the hole region, we `}\n {`retract`}\n {` it by pushing it towards the endpoint, until it reaches the hole region.`}
\n\n\n\n{`What if the intersection was in the other direction?`}\n
\n\n
\n {`\n`}\n
{`Another scenario is when the two tail tangents point to different sides of the base, as below.`}
\n \n{`In this case, any intersections detected are meaningless because they must lie outside the hole region. Instead, we divide the curve into two halves and intrapolate two subcurves from each endpoint to the mid-point of the base as shown above.`}
\n{`The last possible scenario is trivial to handle: when the lines are coincident, simply connect the endpoints with a straight line.`}
\n \n{`The case of our simple ellipse falls into the first scenario. The intrapolated outline is shown as follows:`}
\n \n{`To fill the hole with appropriate colors, we define three element types: `}\n {`Blank`}\n {`, `}\n {`Structure`}\n {`, and `}\n {`Texture`}\n {`. Note that in this project, it is restricted that only one shape is processed at once, and actual texture recovery is not performed - we're only interested in knowing which parts of the hole are Blank, Structure, or Texture.`}
\n{`Element`} | \n\n\n{`Description`} | \n\n
---|---|
{`Blank`} | \n\n\n{`Background pixels. (Black in our demo)`} | \n\n
{`Structure`} | \n\n\n{`Outline of the shape; The intrapolated curve(s) obtained above is rasterized and drawn onto the hole. (Blue in our demo)`} | \n\n
{`Texture`} | \n\n\n{`Solid part of the shape; To be filled in this section. (Red in our demo)`} | \n\n
{`The Structure elements divide the hole into several subregions. Each of these subregions contains wholly either Blank or Texture elements.`}
\n \n{`In our example, the hole is divided by the intrapolated curve into two subregions. In order to guess whether to fill the subregions with Blank or Texture elements, we check if the number of Blank elements outside the hole boundary exceeds a certain tolerance.`}
\n{`For the bottom subregion, the pixels right outside the bottom boundary are (almost) all red (Texture), therefore this subregion is classified as Texture and filled with Texture elements.`}
\n{`For the top subregion, the pixels outside the left, top, and right sides of the boundary are considered. All of those pixels are background (Blank), so this subregion is classified as Blank.`}
\n{`After filling, the shape of the ellipse is completed, as follows:`}
\n \n{`If we move the hole around, shape completion yields the following results:`}
\n \n{`The process of shape completion shown above has been rather straightforward because there is a strong assumption - the hole cuts the shape at exactly 2 endpoints only. Consider the following case:`}
\n \n{`An intuitive approach might be by endpoint proximity in a greedy manner. If we simply connect each endpoint to its nearest neighbor, the correct matching is found for the above case. However, this approach ceases to work for the following case:`}
\n \n{`Problematic matchings are the ones that lead to intersecting curves. If intersection occurs, the resulting shape deforms and there may be subregions that are surrounded by others, leading to problems in color filling. Therefore, the key of endpoint matching lies in `}\n {`avoiding intersections`}\n {`.`}
\n{`Before intrapolation, some intersecting curves can already be identified by looking at endpoint connections that intersect.`}
\n{`Imagine we have 4 endpoints A, B, C, and D. If the line segment AB intersects with CD, then the curve intrapolated from A to B must intersect with that from C to D.`}
\n \n{`Therefore, the first step to avoiding intersecting curves is to filter out matchings that contain intersecting lines.`}
\n{`This `}\n {`webpage`}\n {` shows that minimizing the total length of endpoint connections is equivalent to finding a matching with no intersecting connections. Hence the problem is reduced to a `}\n {`Euclidean Bipartite Matching Problem`}\n {`, i.e. optimizing the global weights over matchings. The `}\n {`Hungarian algorithm`}\n {` is used to solve such a problem.`}
\n{`The rest of the intersecting curves have to be caught and filtered out after intrapolation has taken place. Bézier curve intersection can be detected by a recursive method called `}\n {`De Casteljau's (Bézier Clipping) algorithm`}\n {`, which is implemented in `}\n {`flo_curves`}\n {`, the Bézier curve library we use.`}
\n{`Throughout the process of development, we encountered many edge cases where the pipeline would break down when faced with certain geometrical configurations. For example, an endpoint detected at the corner of the hole may or may not be useful, because it may be an edge entering the hole at a corner, or it may just be the hole touching an edge with its corner.`}
\n{`As much as we hope to correctly handle every single situation (if it's possible in the first place), we figured, from an engineering perspective, that we want an effective way to secure maximum overall robustness with a reasonable amount of effort.`}
\n{`From experimentation, we observed that most of the pain-inducing geometrical configurations were deformed once we move the hole by just 1 pixel. Therefore, we decided that once the pipeline breaks down, it should try to `}\n {`recover by expanding the hole by 1 pixel in each direction`}\n {`. If it successfully produces something in one of these attempts, that result is used.`}
\n{`As shown above, the performance of our implementation is stable for the most part. Occasionally, the pipeline (arguably) incorrectly handles the cases when tail tangents are (nearly) coincident to the hole boundaries.`}
\n \n{`The direct application of this algorithm is symbolic recognition, where a part of a symbol might be obscured artificially or accidentally.`}
\n{`It can also be used in image repairing provided we are able to construct a high level shape structures of a natural photograph.`}
\n{`As only the local information around the unknown region is used, the above shape reconstruction operation is entropy neutral, where no new information is introduced and no priori knowledge is assumed.`}
\n{`This is a key factor, consider that if we have priori knowledge or simply are making wild guesses to what the original symbol might be, it completely changes the problem or actually defies the reason of shape completion.`}
\n{`Under the symbolic recognition framework we propose (will be detailed in another article), recognition is a macroscopic statistical measurement that would greatly benefit from missing pieces filled in.`}
\n\n{`Researchers: `}\n {`Chris Tsang`}\n {` and `}\n {`Sanford Pun`}\n {` | Published: 2021-04-03`}
\n{`This is our story of the design and implementation of a symbolic barcode system.\nTry the Demo Web App on `}\n {`symcode.visioncortex.org`}\n {`.`}
\n \n{`If you are lucky enough to live in a place where an Amazon Go store is nearby, you definitely should visit it. It is packed full of esoteric technology that enthusiast like us get super intrigued. One item that caught our eyes was the labels on the salad and sandwiches in the ready-made food section.`}
\n \n\n\n\n{`Photo by Sikander Iqbal via `}\n {`Wikimedia Commons`}
\n\n
{`There printed a matrix of cute little circles and squares! It must be a barcode of some sort. Our attempt to decipher the '`}\n {`Amazon Reversi Code`}\n {`' was futile, but this is the start of the story.`}
\n{`A bit of context for the unfamiliar reader, the name Barcode is referring to the labels we still find in virtually every packaged products today. Barcode encode data by varying the widths and spacings of the vertical bars, which are to be scanned by a laser beam crossing horizontally. With the advent of image sensors, 2D barcodes were developed, where QR code and Aztec Code are some of the most well-known.`}
\n{`In general, 2D barcode scanner works as follows:`}
\n{`Locate the finders. These finders act as a trigger to tell the scanner that 'here is a barcode!'. The finders have to be easily detectable yet not interfering with the data modules.`}
\n\n{`Attempt to scan and read metadata surrounding the finder candidates. The metadata usually includes a format code and timing patterns.`}
\n\n{`If the previous step succeeds, the scanner would sample the entire barcode to construct a raw bit string. A checksum operation is done to detect errors.`}
\n\n{`If the previous step succeeds, the payload is extract from the raw bits, which usually involves some bit masking and shuffling.`}
\n\n{`This framework has aged well and they are quite robust and low-cost to implement in embedded systems. However, they are meant to be read by machines and not by our naked eyes. They are not designed for aesthetics and are impossible for humans to comprehend.`}
\n{`Our goal is to design a barcode system that are both human-readable and machine-readable. We now turn our attention to human readability by studying human languages.`}
\n{`Machines run on bits, so we can say the alphabet consists of 0 and 1. In human languages, we have a larger set of alphabets. In English, we have 26 distinct lowercase letters. In Korean, characters are constructed by composing 2 to 6 elements from a set of 40 distinct Jamo.`}
\n{`There is a direct tradeoff between information density and visual ambiguity. If the symbol set is too large, humans would have difficulty in remembering all of them. In addition, there may be some visually similar symbols that are hard to disambiguate. If the symbol set is too small, the same message has to be encoded with more symbols, which again, humans often have a hard time in processing long strings.`}
\n{`We determined that a symbol set in the range of 16 to 64 symbols is a good balance.`}
\n{`What makes good symbols?`}
\n{`Prominence`}
\n\n\n{`First, the symbols have to stand out from the natural world, to manifest that they are created deliberately to convey a message but not a result of some natural phenomenon.`}
\n\n{`Repeatable`}
\n\n\n{`Symbols are constructed with specific tools and processes that can be taught to other people. The meaning of a symbol should remain the same when reproduced, in which variation is tolerated.`}
\n\n{`Distinctive`}
\n\n\n{`Symbols within a set should not be similar with each other and have distinctive features allowing the reader to resolve ambiguity between symbols.`}
\n\n{`Aesthetics`}
\n\n\n{`Finally, good symbols should observe the aesthetics rules of the human eye, including being anti-symmetric, rotational symmetric, balanced and consistent. As a pointer, the `}\n {`Gestalt Principles`}\n {` are fantastic rules of thumb.`}
\n\n{`With the above rules in mind, we designed a minimalistic symbol set. Each symbol is composed of multiple triangles, the basic geometric primitive. Each symbol is symmetric or anti-symmetric in overall, but exhibits asymmetry internally. They are like Tangram, in which a human child can easily reproduce the symbols by assembly some triangular pieces together.`}
\n{`The next section would quantitatively analyze and justify this design methodology.`}
\n{`The naive way to match a shape against a symbol set is to perform a linear search, XOR it with every symbol and then chooses the one with the lowest delta. It works in principle, but is inefficient. Ideally, an algorithm can condense a complex shape into a feature vector, which we can lookup in the feature space of the symbol set for the nearest neighbour and arrive to a match.`}
\n{`Instead of using an array of real numbers, we devised that an array of bits are sufficient to capture the essence of symbols, and from now on we refer this bit string as 'trace'.`}
\n{`Now, let us take a closer look at the lowercase English alphabet set to illustrate this idea.`}
\n \n{`First off, we can classify the 26 alphabets as either tall or short, giving:`}
\n{`Tall: b d f g h j k l p q t y\nShort: a c e i m n o r s u v w x z\n`}
\n {`Next, we can divide the letter into two halfs horizontally and compare their weights:`}
\n{`Left > right: b c f h k p r t y\nLeft < right: a d g q\nLeft ~ right: e i j l m n o s u v w x z\n`}
\n {`Then, we can divide the letter into two halfs vertically and compare their weights:`}
\n{`Up > down: f m n p q r y\nUp < down: b d h k u\nUp ~ down: a c e g i j l o s t v w x z\n`}
\n {`At this point, we had the following:`}
\n{`a: SR=\nb: TLD\nc: SL=\nd: TRD\ne: S==\nf: TLU\ng: TR=\nh: TLD\ni: S==\nj: T==\nk: TLD\nl: T==\nm: S=U\nn: S=U\no: S==\np: TLU\nq: TRU\nr: SLU\ns: S==\nt: TLD\nu: S=D\nv: S==\nw: S==\nx: S==\ny: TLU\nz: S==\n`}
\n {`Group by trace:`}
\n{`SR=: a\nTLD: b h k\nSL=: c\nTRD: d\nS==: e i o s v w x z\nTL=: t\nTR=: g\nT==: j l \nS=U: m n\nTLU: f p y\nTRU: q\nSLU: r\nS=D: u\n`}
\n {`Which is a surprisingly good classifier using only three comparisons. We can do more trinary comparisons on smaller partitions to further differentiate the collisions, but our investigation on English alphabets ends here for the scope of this article.`}
\n{`Our trace for SymCode symbols follows a similar scheme. We define the symbol traces starting from the three comparisons defined in the previous section.`}
\n{`The pixel count in each of the four quadrants of a symbol (in order of top-left, top-right, bottom-left, bottom-right) are denoted by non-negative quantities `}\n
{`Below are two examples:`}
\n \n{`Each comparison has `}\n {`3 possible outcomes (`}\n {`<`}\n {`, `}\n {`>`}\n {`, ~)`}\n {`. For simplicity, we assign `}\n {`2 bits`}\n {` to encode each comparison. Therefore, this naive implementation uses `}\n {`3 * 2 = 6 bits`}\n {` to store each trace.`}
\n{`The above worked well enough for sets of 16 symbols, but it was found inadequate for 32 symbols. Acute32 requires more comparisons for traces.`}
\n{`The following figure is Acute32's alphabet set.`}
\n \n{`Apart from the three basic comparisons explained in the previous section (`}\n {`V,H,D`}\n {`), we also compare every pair of the four quadrants (each quadrant is compared to every other quadrant exactly once), requiring `}\n {`an extra of `}\n {`4 choose 2`}\n {` = 6 comparisons`}\n {` (`}\n {`ab, cd, ac`}\n {`, ...). The last comparison `}\n {`efgh`}\n {` is explained in the details below.`}
\n \n{`It is important to note that not any arbitrary extra comparisons are effective. The rule of thumb is each extra comparison should introduce new information than the existing ones, making them `}\n {`(at least partially) independent`}\n {` of each other. In general, comparisons that use `}\n {`different numbers of blocks`}\n {` should be independent. For example, in the previous section all comparisons used 2 blocks vs 2 blocks, so the extra ones in this section, which use 1 block vs 1 block, are all (partially) independent of the previous ones. This is because as more blocks are being considered at once, the scope of analysis becomes irreversibly broader - just like how you cannot retrieve neither `}\n {`x`}\n {` nor `}\n {`y`}\n {` from the sum `}\n {`x+y`}\n {`.`}
\n{`Adding the extra ones results in a total of 3 + 6 = 9 comparisons. Using 2 bits to encode each, we are using 9 * 2 = `}\n {`18 bits`}\n {` to store each trace in Acute32.`}
\n{`Denote a comparison operation by \"U vs V\". The vertical, horizontal, and diagonal comparisons become \"Top vs Bottom\", \"Left vs Right\", and \"Backslash vs Slash\" respectively. The rest of the comparisons become \"a vs b\", \"c vs d\", and so on. We set the bits as follows:`}
\n{`The trace of all 1's, which happens when all four quadrants of the symbol share (approximately) the same number of dots, is shared by about one-third of the entire Acute32. This makes approximately one-third of the searches scan through 12 symbol images, which is 4 times that of most of the rest, which only scan through 3 symbol images.`}
\n{`Our solution here is one more comparison.`}
\n \n{`We further partition the grid of each symbol image so that there are 4x4 = 16 small blocks, and denote the top/bottom/left/right blocks along the edge by `}\n {`e,f,g,h`}\n {` respectively, as illustrated in the figure above. Next, we define an \"`}\n {`ef/gh`}\n {` comparison\" which compares `}\n {`e+f`}\n {` to `}\n {`g+h`}\n {`.`}
\n{`Now we have a more balanced distribution.`}
\n{`The trace partitions the symbol set into smaller, mutually-exclusive subsets. This enhances robustness against noisy inputs.`}
\n{`As searching through traces is much more efficient than comparing with each symbol individually, traces also help with performance.`}
\n{`Most importantly, it provides us a guidance in designing the symbols to maximize differences within the symbol set.`}
\n{`The pipeline of SymCode scanner consists of 4 stages: `}\n
{`Each stage below begins with a more general description of the processing stage (general across any SymCode), followed by explanation of the implementation of Acute32.`}
\n{`We first binarize the input color image using an adaptive thresholding strategy, shapes are then extracted from a binary image by clustering.`}
\n{`The goal of this stage is to find the positions of all finder candidates in the frame.`}
\n{`A minimum of 4 feature points is needed because at least `}\n {`4 point correspondences`}\n {` are required to fit a `}\n {`perspective transform`}\n {`.`}
\n{`Acute32 uses circles as finders because they are distinct from the heavily cornered symbol set. The 4 finders are arranged in a Y shaped manner to break the rotational symmetry with itself.`}
\n{`The advantage of using a circle is that, in general, it always transforms into an ellipse under any perspective distortion, making it relatively easy to detect.`}
\n{`The disadvantage is the `}\n {`lack of corners in circles`}\n {`. It is too easy to detect false positives because there are indeed many circles in real life.`}
\n{`We define the \"`}\n {`image space`}\n {`\" as the space of pixels on the `}\n {`input frame`}\n {`, and the \"`}\n {`object space`}\n {`\" as the space of pixels on the `}\n {`code image`}\n {` (whose boundary is predefined). An image space is simply the input frame image. An object space can either be generated using a code instance, or by rectifying the corresponding image space.`}
\n \n{`In essence, this stage chooses the correct perspective transform to be used in the next stage.`}
\n{`Each perspective transform `}\n {`converts the image space into `}\n {`an`}\n {` object space`}\n {` (but not necessarily the correct one) and `}\n {`is defined by (at least) 4 point pairs`}\n {` (source and destination points), where each pair consists of `}\n {`a point in the image space`}\n {` and `}\n {`the other one in the object space`}\n {`.`}
\n{`Since we have obtained a list of finder candidates from the previous stage, we can extract `}\n {`n`}\n {` feature points in the image space`}\n {` from them. Matching the 4-permutations of them to the `}\n {`4 predefined feature points in the object space`}\n {` gives us at most `}\n {`n permute 4 = k`}\n {` perspective transforms.`}
\n{`It is inefficient to accept each transformation and generate `}\n {`k`}\n {` object spaces to proceed to next stage. Therefore, we need to design a method to evaluate each transform and chooses which ones to proceed. The simplest way is to `}\n {`define some extra feature points in the object space as `}\n {`check points`}\n {`, which are `}\n {`re-projected to the image space`}\n {`, and `}\n {`check if the feature exists there`}\n {`, and reject the candidate otherwise.`}
\n{`The re-projection method mentioned above could not work for us because circles do not provide extra feature points for us to verify.`}
\n{`Instead, we take each of the `}\n {`k`}\n {` perspective transforms and calculates an error value.`}
\n{`We define 4 `}\n {`object check points`}\n {` as the top of the 4 circle finders in the object space. Re-projecting these 4 points to the image space, we obtain the `}\n {`image check points`}\n {` (`}\n {`i1`}\n {` to `}\n {`i4`}\n {`). Furthermore, we denote the `}\n {`centres of the circle finders`}\n {` in the `}\n {`image space`}\n {`, in the same order as the previously defined points, by `}\n {`c1`}\n {` to `}\n {`c4`}\n {`.`}
\n{`Our metric of evaluation relies on `}\n {`magnitude and direction`}\n {` of the difference vector. 4 vectors interest us here: the vectors from the centres of finders to the image check points, and we denote them by `}\n {`v1`}\n {` to `}\n {`v4`}\n {` respectively.`}
\n \n{`We observed that given a correct transform, the 4 green vectors are consistently pointing in the same direction, with their magnitudes less uniform but still very similar. We found it effective to choose the transform with the `}\n {`minimum variance`}\n {` among `}\n {`v1`}\n {` to `}\n {`v4`}\n {`.`}
\n{`In the previous stage, we have obtained a perspective transform which converts between the image and object spaces. Next, we're going to rectify the input image into a code image, and recognize the symbols on it.`}
\n{`Once we have a transform that we believe is correct, the object space can be obtained by applying it on the image space. We sample the image with bilinear interpolation.`}
\n{`Assuming the transform is correct, the coordinates of the symbols on the code image should be close to the `}\n {`anchors`}\n {` we defined in the object space.`}
\n \n{`The bounding boxes in blue are all clusters found on the code image. The boxes in red are the grouped clusters used to recognize each symbol.`}
\n{`Once we have the images, we can `}\n {`evaluate their traces`}\n {` and compare them with the ones in our symbol library, obtaining `}\n {`a small number of candidates`}\n {` for each symbol image. Each of these candidates is compared to the symbol image and the one with the `}\n {`lowest delta`}\n {` is the final recognized symbol.`}
\n{`Each symbol is mapped to a unique bit string, and they are concatenated into a longer bit string.`}
\n{`This stage performs error detection (possibly correction) and extract the payload.`}
\n{`As there are 32 symbols in the set of Acute32, each symbol can encode 5 bits. There are 5 data symbols and 4 finders in the 3x3 configuration. It can easily be extended to a 6x4 to carry more data. For now, each `}\n
{`We developed a theoretic as well as a programming framework for designing and implementing symbolic barcodes. The programming library is available as `}\n
{`This story is a manifest of the philosophy behind Vision Cortex, and a glimpse of the ongoing research and development by the Vision Cortex Research Group.`}
\n{`If you enjoyed the reading and appreciate our work, consider starring (on `}\n {`GitHub`}\n {`), discuss (on `}\n {`Reddit`}\n {` / `}\n {`GitHub`}\n {`), share (on `}\n {`Twitter`}\n {`) and collaborate.`}
\n\n{`Researcher: `}\n {`Sanford Pun`}\n {` | Supervisor: `}\n {`Chris Tsang`}\n {` | Published: 2020-11-01`}
\n{`VTracer is a utility to convert raster images into vector graphics. Try the `}\n {`Demo Web App`}\n {`.`}
\n \n\n\n\n{`Graphic by sunshine-91 via `}\n {`Vecteezy`}
\n\n
{`The input image is first clustered by `}\n {`Hierarchical Clustering`}\n {`, and each of the output clusters are traced into vector.`}
\n{`The algorithm of vector tracing involves 3 main stages:`}
\n{`Convert pixels into path`}
\n\n{`Simplify the path into polygon`}
\n\n{`Smoothen the polygon and approximate it with a curve-fitter`}
\n\n{`VTracer first obtains the raw paths of pixel clusters. A walker is used to trace the outlines of every cluster after building an image tree. The walker would combine consecutive steps in the same direction.`}
\n \n \n{`Path simplification consists of 2 steps:`}
\n{`Remove staircases`}
\n\n{`Simplify by limiting subpath penalties`}
\n\n{`From the previous stage, we have obtained a path whose consecutive edges must have different directions, i.e. the shape is represented by the minimum number of edges with 100% fidelity. However, to represent slant lines and curves in raster graphics, “jaggies” (or pixel staircases) inevitably occur. In this step, we aim at removing these artifacts.`}
\n{`To replace a staircase with a straight line (hence “removing” it), one may adopt an outset (“additive”) or inset (“subtractive”) approach. Both approaches are justifiable in difference contexts, so the key is to maintain consistency in the same shape. In order to determine which points to keep, we make use of the `}\n {`signed area`}\n {`.`}
\n \n{`The signed area of a right triangle is a metric used to determine whether the vertices are arranged clockwise or anti-clockwise geometrically. With this information, we can determine which points to keep on the staircase.`}
\n{`For each point on the staircase, we calculate the signed area of the triangle formed by the previous point, the current point, and the next point on the path. The decision of whether the current point should be kept is then made by comparing the sign of the signed area and the clockwise-ness of the original path.`}
\n \n{`The path can be further simplified by evaluating the `}\n {`penalty`}\n {` from replacing a subpath with one long edge from the first to the last point.`}
\n \n{`Given a subpath, we would like to determine if a line drawn from the first point to the last can approximate the whole subpath with high fidelity. The idea is to make sure that all points in the subpath are close enough to the approximating line. To avoid all the complicated coordinate geometry, we can simply evaluate the areas of triangles formed by the first point, the last point, and each in-between point.`}
\n{`Let ΔABC be one such triangle, with A and C being the first and last points of the subpath respectively, and B being any in-between point. Let h and b be the height and the base (length of AC) respectively. VTracer models the penalty of ΔABC as `}\n
{`Once the penalty is clearly defined, the procedure of simplification is straightforward. VTracer greedily extends a subpath until the maximum penalty along it exceeds a specific tolerance, then all edges in the subpath are replaced by one line from the first to the second last point (or equivalently, remove in-between points from the path). After the replacement, the same process is performed starting from the next point in the path. When the last subpath is extended to the last point in the whole path, the simplification stage concludes.`}
\n \n{`What we have now is a simplified polygon with high fidelity. However, if we feed the path to the curve fitter as is, the curves will approximate the shape poorly. This is because the curves are underdetermined given the small number of points. In order to generate points that lie on our desired shape, subdivision smoothing is performed.`}
\n{`VTracer adapts from the `}\n {`4-Point Scheme`}\n {` subdivision algorithm, which is an interpolating smoothing method. The problem of the 4-Point Scheme is that all vertices would be smoothed into round corners, which, from our point of view, is a loss of fidelity.`}
\n \n \n{`To preserve corners, we first have to locate them. VTracer finds corners by checking the `}\n {`angle difference`}\n {` at each vertex. If the absolute angle difference exceeds a certain threshold, the vertex is considered to be a corner.`}
\n \n{`Angle difference from A to B is small => a is not a corner\n`}\n
\n {`\nAngle difference from B to C is large => b is a corner`}
{`In the original 4-Point Scheme, 2 adjacent points are always used to generate the new point for each segment. In our adapted version, we do not take the adjacent point for corners, but instead we take the corners themselves. For segments whose points are both corners, we simply ignore them.`}
\n \n{`Since A₂ is a corner, the smoothing procedure does not take the adjacent point as B₂. As a result, the corner will be (approximately) preserved after smoothing, even after iterations.`}
\n{`VTracer applies a threshold on the length of each segment during subdivision smoothing, so that the result will not be over-dense. This threshold should be decided carefully (mainly based on the resolution of image), otherwise the resulting path will be a poor approximation.`}
\n{`Shown below are examples smoothed with no iteration:`}
\n \n{`Introducing iterations, you can see more points are generated by subdivision:`}
\n \n{`VTracer's implementation defaults to 10 iterations, and exit early when no further smoothing can be done on the path.`}
\n{`The path is now populated with nicely positioned and sufficiently dense points that faithfully represent our desired shape. Before feeding it to a (Bezier) curve-fitter, VTracer determines where to cut curves (splice points).`}
\n{`To define a splice point, we make use of the `}\n {`signed angle differences`}\n {`. Interpreting each edge along the path as vectors, we can define the signed angle difference from edge eᵢ to eᵢ₊₁ as the 2D angle of rotation θ ∊ (-π, π] required to rotate eᵢ to the same direction as eᵢ₊₁ (assume positive θ in clockwise direction).`}
\n{`It is sufficient for a vertex to be a splice point if it passes one of two tests: `}\n {`point of inflection test`}\n {`and `}\n {`angle displacement test`}\n {`.`}
\n{`Points of inflection can be found by tracking the signs of angle differences along the path. When the concavity of the path changes at a certain point, the sign of the signed angle difference also changes at that point.`}
\n \n{`As the sign of angle difference changes at point P, P is a point of inflection and hence a splice point.\n`}\n
\n {`\nTherefore, we cut (A..B) into (A..P) and (P..B).`}
{`Angle displacement at a point is defined as the signed angle differences accumulated from the previous splice point (exclusive) to the current point (inclusive). If the absolute value of angle displacement at a certain point exceeds a specific threshold, that point is a splice point. The smaller the threshold, the more curves are cut, and the resulting spline is more accurate.`}
\n \n{`Once all splice points are found, VTracer feeds all subpaths between every consecutive pair of splice points into the curve-fitter.`}
\n{`If the smoothed path from the previous step is fed into the curve-fitter, we get a spline like the following:`}
\n \n{`Potrace`}\n {` is a popular bitmap tracing tool that also transforms bitmaps into vector graphics. Being able to produce high-quality output for low-resolution images, Potrace traces images by finding global optimal under certain assumptions. VTracer favours fidelity over simplification. Potrace and VTracer produce different results especially on small objects.`}
\n \n{`Illustration of how assumptions affect tracing.\n`}\n
\n {`\nLeft: Original input.\n`}\n
\n {`\nMiddle: Possible shape interpretation with the assumption that ambiguous “corners” in the original input are sharp corners.\n`}\n
\n {`Right: Possible shape interpretation with no assumptions: “corners” in the original image are represented by curves or round corners.`}
{`Potrace finds the global optimal way of tracing a shape, meaning it approximates parts using information from the entire shape, which leads to performance issues in high-resolution images. On the other hand, VTracer runs a linear algorithm on each cluster and has lower CPU and memory usage. In fact, we regularly use VTracer to process high resolution scans at 10 megapixels.`}
\n{`Comparing to Adobe Illustrator's Live Trace, VTracer's output is much more compact thanks to a stacking strategy. VTracer's preprocessing stage utilizes Vision Cortex's image tree algorithm and never produces shapes with holes.`}
\n\nP(a,n))void 0!==u&&0>P(u,a)?(e[r]=u,e[o]=n,r=o):(e[r]=a,e[i]=n,r=i);else{if(!(void 0!==u&&0>P(u,n)))break e;e[r]=u,e[o]=n,r=o}}}return t}return null}function P(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}var N=[],z=[],O=1,R=null,M=3,I=!1,F=!1,D=!1;function L(e){for(var t=C(z);null!==t;){if(null===t.callback)_(z);else{if(!(t.startTime<=e))break;_(z),t.sortIndex=t.expirationTime,S(N,t)}t=C(z)}}function A(e){if(D=!1,L(e),!F)if(null!==C(N))F=!0,r(U);else{var t=C(z);null!==t&&l(A,t.startTime-e)}}function U(e,n){F=!1,D&&(D=!1,i()),I=!0;var r=M;try{for(L(n),R=C(N);null!==R&&(!(R.expirationTime>n)||e&&!a());){var o=R.callback;if(null!==o){R.callback=null,M=R.priorityLevel;var u=o(R.expirationTime<=n);n=t.unstable_now(),"function"==typeof u?R.callback=u:R===C(N)&&_(N),L(n)}else _(N);R=C(N)}if(null!==R)var c=!0;else{var s=C(z);null!==s&&l(A,s.startTime-n),c=!1}return c}finally{R=null,M=r,I=!1}}function V(e){switch(e){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1e4;default:return 5e3}}var W=o;t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_continueExecution=function(){F||I||(F=!0,r(U))},t.unstable_getCurrentPriorityLevel=function(){return M},t.unstable_getFirstCallbackNode=function(){return C(N)},t.unstable_next=function(e){switch(M){case 1:case 2:case 3:var t=3;break;default:t=M}var n=M;M=t;try{return e()}finally{M=n}},t.unstable_pauseExecution=function(){},t.unstable_requestPaint=W,t.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=M;M=e;try{return t()}finally{M=n}},t.unstable_scheduleCallback=function(e,n,a){var o=t.unstable_now();if("object"==typeof a&&null!==a){var u=a.delay;u="number"==typeof u&&0o?(e.sortIndex=u,S(z,e),null===C(N)&&e===C(z)&&(D?i():D=!0,l(A,u-o))):(e.sortIndex=a,S(N,e),F||I||(F=!0,r(U))),e},t.unstable_shouldYield=function(){var e=t.unstable_now();L(e);var n=C(N);return n!==R&&null!==R&&null!==n&&null!==n.callback&&n.startTime<=e&&n.expirationTime Researcher: Chris Tsang | Published: 2020-12-24 Impression is a family of algorithms for image simplification, segmentation and vectorization. Try the Demo Web App. The fundamental information result from visual perception is shape. For example, we would describe ‘there is an orange on a basket’ or more conceptually ‘there is a circle above a triangle’. If we regard simplification as a process of information reduction, we could imagine information being taken away from an image shape by shape, starting from the least important shapes. At the same time, we can also simplify each shape in the image to make it ‘less fractal’. By controlling this reduction process, we would be able to control the amount of visual information in an image in a quantitative manner. Under this understanding, segmentation is an extreme degree of simplification, while vectorization is a slight degree of simplification. The clustering algorithm is hierarchical in nature and the idea is best described by the following verse: On an unclaimed land, there are villages. Villages ally with each other to form tribes. Tribes conquer each other and form Kingdoms. Kingdoms clashes with each other and form empires. Empires breakdown and finally the entire earth becomes one union. Clustering is a process to connect pixels of similar colors into clusters. Each cluster have a unique position and shape. There is well known algorithm to perform clustering and Impression’s implementation is essentially same as Kopelman Algorithm. Hierarchical clustering is a process to build an image tree from the clusters of stage 1. Impression builds a binary tree bottom up. In preparation, clusters are sorted by their size. Starting from the smallest, in each iteration, a cluster is merged into the closest cluster. There are different formulas for ‘close’, but it generally involves comparing the average colors between neighboring clusters. In image vectorization as in VTracer, we would walk the tree from top to bottom, trace each layer, and stack the vector paths on the output canvas. As if a painter paints on an empty canvas, he would lay down background first and then overlay objects atop, and finally add on details. In image simplification, there are multiple dimensions of information which can be controlled quantitatively: Impression currently chose the Ramer-Douglas-Peucker algorithm for shape simplification. While the original algorithm is designed for open curves, Impression adapted it for closed shapes. We cut a closed path into 4 sections, simplify each, and stitch them back together after path simplification. We choose the north most, east most, south most and west most points to cut a given path. Effectively the simplest possible shape is a diamond with four points. However in practice, it is not desirable to have shapes with sharp corners, and so we would smooth them with a 4-point scheme as described in VTracer. It has to be understood that statistically there are exponentially more nodes as we decrease the cluster size. There is one root node known as the image background, but as many leaf nodes as there are number of pixels. As such, fidelity is actually a cutoff which we discard all clusters smaller than a desired size. When this cutoff threshold is low, we are removing salt and pepper noise but in a true color sense. As we increase this cutoff, structures are being discarded. Further increasing this cutoff, the image would become more abstract, and in the end only one solid background will be left. It is important to note that this background color is not the mean pixel color of the image, but is the average color of the largest cluster, which conceptually is the ‘base tone’ of the given image. More color levels mean finer gradient. Color levels set to 256 means utilizing the full color precision of RGB888. Setting color levels to 32 meaning a cluster has to have at least a color difference of 8 in order to be considered a separate cluster with the cluster on the upper level. In effect, tuning down the color levels would create a retro 8-bit color look. While the number of colors in the palette is limited, the colors are still 24-bit and thus are faithful to the original image. Segmentation follows stage 3 of the algorithm. Similar clusters are grouped together by the disjoint sets algorithm, where each set of the result represents a solid color patch. For each cluster, its neighbours are being considered. If the color difference is smaller than a defined deviation, they will be put into the same union. To prevent grouping too greedily, after the first union, subsequent union would require stricter thresholds. To output segmentation result, each set would be rendered by the average color of all its constituting clusters. After this stage, there would still be unwanted patches in the output. The output would further be re-clustered by Stage 1 to 3 described before, and an aggregation pass be applied afterwards. It would prune away smaller patches by merging into the closest (in terms of color) cluster if deviation would allow. Finally, to output result, each aggregate would be rendered by its original cluster color. The described algorithms are implemented as reusable components under VisionMagic. Together with a processing pipeline, they are designed to support real-time and interactive applications. Users can easily organize processing stages and add additional processing passes as needed. The The above methodology is designed to imitate visual perception in humans. It is thus no surprising that resulting images exhibit characteristics similar to paintings drawn by Artists. As the name suggests, it is heavy inspired by a painting technique that we called ‘Impressionism’ for the meaning it convey during a brief period of art history. Building photo by Hernan Lucio on Unsplash Dog photo by Elijah Ekdahl on Unsplash Cityscape photo by Mark Denton on Unsplash Landscape photo by Luca Bravo on Unsplash Parrot photo by Sanved Bangale on Unsplash Water lily photo by Ravi Sharma on Unsplash The goal of Semantic Computer Vision is to allow computers to understand the content of images and graphics as intended and perceived by humans, and construct a high level representation of such information. This technology can be embodied in different applications: We developed SymCode, a 2D barcode designed to be both human-readable and machine-readable. We developed Impression, a family of algorithms for image simplification and segmentation. It allows us to control the amount of visual information in an image in a quantitative manner. We developed VTracer, a utility to convert raster images (like jpg & png) into vector graphics (svg). Our graphics pipeline is able to process high resolution scans and photographs and trace the content to output compact vector files. We are developing a new Optical Character Recognition (OCR) engine from the ground up specifically for pictorial languages like Chinese, Korean and Japanese (CKJ) to encompass wider character sets and font variations. Researcher: Chris Tsang | Published: 2021-04-01 All we know about the barcode that somehow resembles a Reversi game. Photo by GoToVan via Wikimedia Commons Disclaimer: I have never been to an Amazon Go store myself. All samples are collected from the internet, and I merely compiled my observations as below. I never received any hints from anyone related to Amazon, so my design and analysis of the Reversi Code is 'clean room'. Observations: The matrix is 6x4. There are exactly three diamonds, which can be placed anywhere on the matrix. 15 to 17 of them are occupied by circles or diamonds, meaning there are 12 to 14 circles. If we number the modules from top left to bottom right, as in The 0th, 4th, 11th, 12th, 19th and 23rd module are always occupied. My guess is they act as a timing pattern. Either 5th or 18th module is occupied, but never both, my guess is they act as an orientation marker (otherwise the code will be rotational symmetric with itself). The remaining 16 positions can freely toggle. Due to #3, 8 to 10 of them must be occupied. There are (16 choose 8) + (16 choose 9) + (16 choose 10) = 32318 combinations. The diamonds can be put on any occupied position, so it multiplies an extra (17 choose 3) + (16 choose 3) + (15 choose 3) = 1695 combinations. It results in 54779010 combinations, just above 2^25 = 33554432
+Which is reasonable enough, as it is a 6x4 matrix with a little extra freedom. If we set aside 5 bits as redundancy, then the Reversi Code can encode 20 bits payload. This is a reasonable guess in the sense that 20% redundancy provides good error detection capability. This is merely a combinatorial analysis representing the theoretic capacity of the barcode. Even given answers of the payload, it is extremely hard to reverse engineer the complete encoding scheme. There is a decimal number above every barcode on each label. From the layout of the label, we believe that it ranges from 000 to 999. It may or may not directly corresponds to the numeric value of the barcode. Now, we can engineer a barcode that looks exactly the same, but encode data in a different way. Researcher: Sanford Pun | Supervisor: Chris Tsang | Published: 2022-01-02 Repository: visioncortex/ShapeSense This project aims at recovering (completing) the structure of a shape after a portion of it is erased. In other words, given a shape with arbitrarily "discontinued parts", assuming that we have no prior knowledge about the shape, how can we reasonably connect the endpoints at the discontinuities? In the demo images shown throughout this documentation, the following coloring scheme is used: Black pixels denote the background. Red pixels denote the rasterized shape. White pixels denote the hole. The (imaginary) intersection of the shape and the hole is the aforementioned "discontinued parts", which is what we try to recover. Blue pixels will be used to denote the outline of the recovered parts of the shape. The whole process of shape completion involves intrapolating the missing outline and then filling the pixels in the hole with appropriate colors. Let's begin the experiment with simple shapes, like an ellipse. The first stage of the pipeline is to obtain and process the paths (curves) representing the existing outline of the shape. Vision Cortex's core library provides the necessary utilities to extract raw paths from an image. The next step is to (divide, if needed, and) extract the two curves from the two endpoints; smoothing is performed to better approximate the tangents near the endpoints (tails of the whole curve). After this step, we will obtain two tangents (2-D direction vectors), one at each tail. We will call these tangents tail tangents. A number of factors determine the accuracy and robustness of tail tangent approximation. Our implementation supports configurations like how many points to consider from the tails, how long should the segments being considered accumulate to, and how the weights for each segment should change towards the tails. Why "Intrapolation"? With the two endpoints and their corresponding tail tangents, we can calculate for the missing part in different scenarios. The type of curves used in this project is cubic Bézier curves. To specify such a curve, four points are required. An important property to note about this type of curves is that the curve constructed is always fully contained in the quadrilateral defined by the four points. The first scenario is when the two tail tangents point to the same side with respect to the line connecting the two endpoints (we call this line the base). To construct the curve between A and B, we need to identify two control points (CA and CB) between them. In our approach, we started with what we think is intuitive and made tweaks to resolve some issues in practice; this is what we end up with: First, find the intersection of the two lines starting at A and B along the corresponding tail tangent. The mid-points between A/B and the intersection are then set to be CA and CB. If the intersection is too far away (i.e. the two lines are close to, if not exactly, parallel), we simply use a point on each line as the control points (e.g. translate A/B along their tail tangent by a factor of base length). Either way, if either CA or CB end up lying outside the hole region, we retract it by pushing it towards the endpoint, until it reaches the hole region. What if the intersection was in the other direction? Another scenario is when the two tail tangents point to different sides of the base, as below. In this case, any intersections detected are meaningless because they must lie outside the hole region. Instead, we divide the curve into two halves and intrapolate two subcurves from each endpoint to the mid-point of the base as shown above. The last possible scenario is trivial to handle: when the lines are coincident, simply connect the endpoints with a straight line. The case of our simple ellipse falls into the first scenario. The intrapolated outline is shown as follows: To fill the hole with appropriate colors, we define three element types: Blank, Structure, and Texture. Note that in this project, it is restricted that only one shape is processed at once, and actual texture recovery is not performed - we're only interested in knowing which parts of the hole are Blank, Structure, or Texture. The Structure elements divide the hole into several subregions. Each of these subregions contains wholly either Blank or Texture elements. In our example, the hole is divided by the intrapolated curve into two subregions. In order to guess whether to fill the subregions with Blank or Texture elements, we check if the number of Blank elements outside the hole boundary exceeds a certain tolerance. For the bottom subregion, the pixels right outside the bottom boundary are (almost) all red (Texture), therefore this subregion is classified as Texture and filled with Texture elements. For the top subregion, the pixels outside the left, top, and right sides of the boundary are considered. All of those pixels are background (Blank), so this subregion is classified as Blank. After filling, the shape of the ellipse is completed, as follows: If we move the hole around, shape completion yields the following results: The process of shape completion shown above has been rather straightforward because there is a strong assumption - the hole cuts the shape at exactly 2 endpoints only. Consider the following case: An intuitive approach might be by endpoint proximity in a greedy manner. If we simply connect each endpoint to its nearest neighbor, the correct matching is found for the above case. However, this approach ceases to work for the following case: Problematic matchings are the ones that lead to intersecting curves. If intersection occurs, the resulting shape deforms and there may be subregions that are surrounded by others, leading to problems in color filling. Therefore, the key of endpoint matching lies in avoiding intersections. Before intrapolation, some intersecting curves can already be identified by looking at endpoint connections that intersect. Imagine we have 4 endpoints A, B, C, and D. If the line segment AB intersects with CD, then the curve intrapolated from A to B must intersect with that from C to D. Therefore, the first step to avoiding intersecting curves is to filter out matchings that contain intersecting lines. This webpage shows that minimizing the total length of endpoint connections is equivalent to finding a matching with no intersecting connections. Hence the problem is reduced to a Euclidean Bipartite Matching Problem, i.e. optimizing the global weights over matchings. The Hungarian algorithm is used to solve such a problem. The rest of the intersecting curves have to be caught and filtered out after intrapolation has taken place. Bézier curve intersection can be detected by a recursive method called De Casteljau's (Bézier Clipping) algorithm, which is implemented in flo_curves, the Bézier curve library we use. Throughout the process of development, we encountered many edge cases where the pipeline would break down when faced with certain geometrical configurations. For example, an endpoint detected at the corner of the hole may or may not be useful, because it may be an edge entering the hole at a corner, or it may just be the hole touching an edge with its corner. As much as we hope to correctly handle every single situation (if it's possible in the first place), we figured, from an engineering perspective, that we want an effective way to secure maximum overall robustness with a reasonable amount of effort. From experimentation, we observed that most of the pain-inducing geometrical configurations were deformed once we move the hole by just 1 pixel. Therefore, we decided that once the pipeline breaks down, it should try to recover by expanding the hole by 1 pixel in each direction. If it successfully produces something in one of these attempts, that result is used. As shown above, the performance of our implementation is stable for the most part. Occasionally, the pipeline (arguably) incorrectly handles the cases when tail tangents are (nearly) coincident to the hole boundaries. The direct application of this algorithm is symbolic recognition, where a part of a symbol might be obscured artificially or accidentally. It can also be used in image repairing provided we are able to construct a high level shape structures of a natural photograph. As only the local information around the unknown region is used, the above shape reconstruction operation is entropy neutral, where no new information is introduced and no priori knowledge is assumed. This is a key factor, consider that if we have priori knowledge or simply are making wild guesses to what the original symbol might be, it completely changes the problem or actually defies the reason of shape completion. Under the symbolic recognition framework we propose (will be detailed in another article), recognition is a macroscopic statistical measurement that would greatly benefit from missing pieces filled in. Researchers: Chris Tsang and Sanford Pun | Published: 2021-04-03 This is our story of the design and implementation of a symbolic barcode system.
+Try the Demo Web App on symcode.visioncortex.org. If you are lucky enough to live in a place where an Amazon Go store is nearby, you definitely should visit it. It is packed full of esoteric technology that enthusiast like us get super intrigued. One item that caught our eyes was the labels on the salad and sandwiches in the ready-made food section. Photo by Sikander Iqbal via Wikimedia Commons There printed a matrix of cute little circles and squares! It must be a barcode of some sort. Our attempt to decipher the 'Amazon Reversi Code' was futile, but this is the start of the story. A bit of context for the unfamiliar reader, the name Barcode is referring to the labels we still find in virtually every packaged products today. Barcode encode data by varying the widths and spacings of the vertical bars, which are to be scanned by a laser beam crossing horizontally. With the advent of image sensors, 2D barcodes were developed, where QR code and Aztec Code are some of the most well-known. In general, 2D barcode scanner works as follows: Locate the finders. These finders act as a trigger to tell the scanner that 'here is a barcode!'. The finders have to be easily detectable yet not interfering with the data modules. Attempt to scan and read metadata surrounding the finder candidates. The metadata usually includes a format code and timing patterns. If the previous step succeeds, the scanner would sample the entire barcode to construct a raw bit string. A checksum operation is done to detect errors. If the previous step succeeds, the payload is extract from the raw bits, which usually involves some bit masking and shuffling. This framework has aged well and they are quite robust and low-cost to implement in embedded systems. However, they are meant to be read by machines and not by our naked eyes. They are not designed for aesthetics and are impossible for humans to comprehend. Our goal is to design a barcode system that are both human-readable and machine-readable. We now turn our attention to human readability by studying human languages. Machines run on bits, so we can say the alphabet consists of 0 and 1. In human languages, we have a larger set of alphabets. In English, we have 26 distinct lowercase letters. In Korean, characters are constructed by composing 2 to 6 elements from a set of 40 distinct Jamo. There is a direct tradeoff between information density and visual ambiguity. If the symbol set is too large, humans would have difficulty in remembering all of them. In addition, there may be some visually similar symbols that are hard to disambiguate. If the symbol set is too small, the same message has to be encoded with more symbols, which again, humans often have a hard time in processing long strings. We determined that a symbol set in the range of 16 to 64 symbols is a good balance. What makes good symbols? Prominence First, the symbols have to stand out from the natural world, to manifest that they are created deliberately to convey a message but not a result of some natural phenomenon. Repeatable Symbols are constructed with specific tools and processes that can be taught to other people. The meaning of a symbol should remain the same when reproduced, in which variation is tolerated. Distinctive Symbols within a set should not be similar with each other and have distinctive features allowing the reader to resolve ambiguity between symbols. Aesthetics Finally, good symbols should observe the aesthetics rules of the human eye, including being anti-symmetric, rotational symmetric, balanced and consistent. As a pointer, the Gestalt Principles are fantastic rules of thumb. With the above rules in mind, we designed a minimalistic symbol set. Each symbol is composed of multiple triangles, the basic geometric primitive. Each symbol is symmetric or anti-symmetric in overall, but exhibits asymmetry internally. They are like Tangram, in which a human child can easily reproduce the symbols by assembly some triangular pieces together. The next section would quantitatively analyze and justify this design methodology. The naive way to match a shape against a symbol set is to perform a linear search, XOR it with every symbol and then chooses the one with the lowest delta. It works in principle, but is inefficient. Ideally, an algorithm can condense a complex shape into a feature vector, which we can lookup in the feature space of the symbol set for the nearest neighbour and arrive to a match. Instead of using an array of real numbers, we devised that an array of bits are sufficient to capture the essence of symbols, and from now on we refer this bit string as 'trace'. Now, let us take a closer look at the lowercase English alphabet set to illustrate this idea. First off, we can classify the 26 alphabets as either tall or short, giving: Next, we can divide the letter into two halfs horizontally and compare their weights: Then, we can divide the letter into two halfs vertically and compare their weights: At this point, we had the following: Group by trace: Which is a surprisingly good classifier using only three comparisons. We can do more trinary comparisons on smaller partitions to further differentiate the collisions, but our investigation on English alphabets ends here for the scope of this article. Our trace for SymCode symbols follows a similar scheme. We define the symbol traces starting from the three comparisons defined in the previous section. The pixel count in each of the four quadrants of a symbol (in order of top-left, top-right, bottom-left, bottom-right) are denoted by non-negative quantities Below are two examples: Each comparison has 3 possible outcomes (<, >, ~). For simplicity, we assign 2 bits to encode each comparison. Therefore, this naive implementation uses 3 * 2 = 6 bits to store each trace. The above worked well enough for sets of 16 symbols, but it was found inadequate for 32 symbols. Acute32 requires more comparisons for traces. The following figure is Acute32's alphabet set. Apart from the three basic comparisons explained in the previous section (V,H,D), we also compare every pair of the four quadrants (each quadrant is compared to every other quadrant exactly once), requiring an extra of 4 choose 2 = 6 comparisons (ab, cd, ac, ...). The last comparison efgh is explained in the details below. It is important to note that not any arbitrary extra comparisons are effective. The rule of thumb is each extra comparison should introduce new information than the existing ones, making them (at least partially) independent of each other. In general, comparisons that use different numbers of blocks should be independent. For example, in the previous section all comparisons used 2 blocks vs 2 blocks, so the extra ones in this section, which use 1 block vs 1 block, are all (partially) independent of the previous ones. This is because as more blocks are being considered at once, the scope of analysis becomes irreversibly broader - just like how you cannot retrieve neither x nor y from the sum x+y. Adding the extra ones results in a total of 3 + 6 = 9 comparisons. Using 2 bits to encode each, we are using 9 * 2 = 18 bits to store each trace in Acute32. Denote a comparison operation by "U vs V". The vertical, horizontal, and diagonal comparisons become "Top vs Bottom", "Left vs Right", and "Backslash vs Slash" respectively. The rest of the comparisons become "a vs b", "c vs d", and so on. We set the bits as follows: The trace of all 1's, which happens when all four quadrants of the symbol share (approximately) the same number of dots, is shared by about one-third of the entire Acute32. This makes approximately one-third of the searches scan through 12 symbol images, which is 4 times that of most of the rest, which only scan through 3 symbol images. Our solution here is one more comparison. We further partition the grid of each symbol image so that there are 4x4 = 16 small blocks, and denote the top/bottom/left/right blocks along the edge by e,f,g,h respectively, as illustrated in the figure above. Next, we define an "ef/gh comparison" which compares e+f to g+h. Now we have a more balanced distribution. The trace partitions the symbol set into smaller, mutually-exclusive subsets. This enhances robustness against noisy inputs. As searching through traces is much more efficient than comparing with each symbol individually, traces also help with performance. Most importantly, it provides us a guidance in designing the symbols to maximize differences within the symbol set. The pipeline of SymCode scanner consists of 4 stages: Each stage below begins with a more general description of the processing stage (general across any SymCode), followed by explanation of the implementation of Acute32. We first binarize the input color image using an adaptive thresholding strategy, shapes are then extracted from a binary image by clustering. The goal of this stage is to find the positions of all finder candidates in the frame. A minimum of 4 feature points is needed because at least 4 point correspondences are required to fit a perspective transform. Acute32 uses circles as finders because they are distinct from the heavily cornered symbol set. The 4 finders are arranged in a Y shaped manner to break the rotational symmetry with itself. The advantage of using a circle is that, in general, it always transforms into an ellipse under any perspective distortion, making it relatively easy to detect. The disadvantage is the lack of corners in circles. It is too easy to detect false positives because there are indeed many circles in real life. We define the "image space" as the space of pixels on the input frame, and the "object space" as the space of pixels on the code image (whose boundary is predefined). An image space is simply the input frame image. An object space can either be generated using a code instance, or by rectifying the corresponding image space. In essence, this stage chooses the correct perspective transform to be used in the next stage. Each perspective transform converts the image space into an object space (but not necessarily the correct one) and is defined by (at least) 4 point pairs (source and destination points), where each pair consists of a point in the image space and the other one in the object space. Since we have obtained a list of finder candidates from the previous stage, we can extract n feature points in the image space from them. Matching the 4-permutations of them to the 4 predefined feature points in the object space gives us at most n permute 4 = k perspective transforms. It is inefficient to accept each transformation and generate k object spaces to proceed to next stage. Therefore, we need to design a method to evaluate each transform and chooses which ones to proceed. The simplest way is to define some extra feature points in the object space as check points, which are re-projected to the image space, and check if the feature exists there, and reject the candidate otherwise. The re-projection method mentioned above could not work for us because circles do not provide extra feature points for us to verify. Instead, we take each of the k perspective transforms and calculates an error value. We define 4 object check points as the top of the 4 circle finders in the object space. Re-projecting these 4 points to the image space, we obtain the image check points (i1 to i4). Furthermore, we denote the centres of the circle finders in the image space, in the same order as the previously defined points, by c1 to c4. Our metric of evaluation relies on magnitude and direction of the difference vector. 4 vectors interest us here: the vectors from the centres of finders to the image check points, and we denote them by v1 to v4 respectively. We observed that given a correct transform, the 4 green vectors are consistently pointing in the same direction, with their magnitudes less uniform but still very similar. We found it effective to choose the transform with the minimum variance among v1 to v4. In the previous stage, we have obtained a perspective transform which converts between the image and object spaces. Next, we're going to rectify the input image into a code image, and recognize the symbols on it. Once we have a transform that we believe is correct, the object space can be obtained by applying it on the image space. We sample the image with bilinear interpolation. Assuming the transform is correct, the coordinates of the symbols on the code image should be close to the anchors we defined in the object space. The bounding boxes in blue are all clusters found on the code image. The boxes in red are the grouped clusters used to recognize each symbol. Once we have the images, we can evaluate their traces and compare them with the ones in our symbol library, obtaining a small number of candidates for each symbol image. Each of these candidates is compared to the symbol image and the one with the lowest delta is the final recognized symbol. Each symbol is mapped to a unique bit string, and they are concatenated into a longer bit string. This stage performs error detection (possibly correction) and extract the payload. As there are 32 symbols in the set of Acute32, each symbol can encode 5 bits. There are 5 data symbols and 4 finders in the 3x3 configuration. It can easily be extended to a 6x4 to carry more data. For now, each We developed a theoretic as well as a programming framework for designing and implementing symbolic barcodes. The programming library is available as This story is a manifest of the philosophy behind Vision Cortex, and a glimpse of the ongoing research and development by the Vision Cortex Research Group. If you enjoyed the reading and appreciate our work, consider starring (on GitHub), discuss (on Reddit / GitHub), share (on Twitter) and collaborate. Researcher: Sanford Pun | Supervisor: Chris Tsang | Published: 2020-11-01 VTracer is a utility to convert raster images into vector graphics. Try the Demo Web App. Graphic by sunshine-91 via Vecteezy The input image is first clustered by Hierarchical Clustering, and each of the output clusters are traced into vector. The algorithm of vector tracing involves 3 main stages: Convert pixels into path Simplify the path into polygon Smoothen the polygon and approximate it with a curve-fitter VTracer first obtains the raw paths of pixel clusters. A walker is used to trace the outlines of every cluster after building an image tree. The walker would combine consecutive steps in the same direction. Path simplification consists of 2 steps: Remove staircases Simplify by limiting subpath penalties From the previous stage, we have obtained a path whose consecutive edges must have different directions, i.e. the shape is represented by the minimum number of edges with 100% fidelity. However, to represent slant lines and curves in raster graphics, “jaggies” (or pixel staircases) inevitably occur. In this step, we aim at removing these artifacts. To replace a staircase with a straight line (hence “removing” it), one may adopt an outset (“additive”) or inset (“subtractive”) approach. Both approaches are justifiable in difference contexts, so the key is to maintain consistency in the same shape. In order to determine which points to keep, we make use of the signed area. The signed area of a right triangle is a metric used to determine whether the vertices are arranged clockwise or anti-clockwise geometrically. With this information, we can determine which points to keep on the staircase. For each point on the staircase, we calculate the signed area of the triangle formed by the previous point, the current point, and the next point on the path. The decision of whether the current point should be kept is then made by comparing the sign of the signed area and the clockwise-ness of the original path. The path can be further simplified by evaluating the penalty from replacing a subpath with one long edge from the first to the last point. Given a subpath, we would like to determine if a line drawn from the first point to the last can approximate the whole subpath with high fidelity. The idea is to make sure that all points in the subpath are close enough to the approximating line. To avoid all the complicated coordinate geometry, we can simply evaluate the areas of triangles formed by the first point, the last point, and each in-between point. Let ΔABC be one such triangle, with A and C being the first and last points of the subpath respectively, and B being any in-between point. Let h and b be the height and the base (length of AC) respectively. VTracer models the penalty of ΔABC as , as the area of ΔABC and b can be obtained by simple geometry. It is crucial for the penalty to be directly proportional to h and b. Once the penalty is clearly defined, the procedure of simplification is straightforward. VTracer greedily extends a subpath until the maximum penalty along it exceeds a specific tolerance, then all edges in the subpath are replaced by one line from the first to the second last point (or equivalently, remove in-between points from the path). After the replacement, the same process is performed starting from the next point in the path. When the last subpath is extended to the last point in the whole path, the simplification stage concludes. What we have now is a simplified polygon with high fidelity. However, if we feed the path to the curve fitter as is, the curves will approximate the shape poorly. This is because the curves are underdetermined given the small number of points. In order to generate points that lie on our desired shape, subdivision smoothing is performed. VTracer adapts from the 4-Point Scheme subdivision algorithm, which is an interpolating smoothing method. The problem of the 4-Point Scheme is that all vertices would be smoothed into round corners, which, from our point of view, is a loss of fidelity. To preserve corners, we first have to locate them. VTracer finds corners by checking the angle difference at each vertex. If the absolute angle difference exceeds a certain threshold, the vertex is considered to be a corner. Angle difference from A to B is small => a is not a corner
+ In the original 4-Point Scheme, 2 adjacent points are always used to generate the new point for each segment. In our adapted version, we do not take the adjacent point for corners, but instead we take the corners themselves. For segments whose points are both corners, we simply ignore them. Since A₂ is a corner, the smoothing procedure does not take the adjacent point as B₂. As a result, the corner will be (approximately) preserved after smoothing, even after iterations. VTracer applies a threshold on the length of each segment during subdivision smoothing, so that the result will not be over-dense. This threshold should be decided carefully (mainly based on the resolution of image), otherwise the resulting path will be a poor approximation. Shown below are examples smoothed with no iteration: Introducing iterations, you can see more points are generated by subdivision: VTracer's implementation defaults to 10 iterations, and exit early when no further smoothing can be done on the path. The path is now populated with nicely positioned and sufficiently dense points that faithfully represent our desired shape. Before feeding it to a (Bezier) curve-fitter, VTracer determines where to cut curves (splice points). To define a splice point, we make use of the signed angle differences. Interpreting each edge along the path as vectors, we can define the signed angle difference from edge eᵢ to eᵢ₊₁ as the 2D angle of rotation θ ∊ (-π, π] required to rotate eᵢ to the same direction as eᵢ₊₁ (assume positive θ in clockwise direction). It is sufficient for a vertex to be a splice point if it passes one of two tests: point of inflection testand angle displacement test. Points of inflection can be found by tracking the signs of angle differences along the path. When the concavity of the path changes at a certain point, the sign of the signed angle difference also changes at that point. As the sign of angle difference changes at point P, P is a point of inflection and hence a splice point.
+ Angle displacement at a point is defined as the signed angle differences accumulated from the previous splice point (exclusive) to the current point (inclusive). If the absolute value of angle displacement at a certain point exceeds a specific threshold, that point is a splice point. The smaller the threshold, the more curves are cut, and the resulting spline is more accurate. Once all splice points are found, VTracer feeds all subpaths between every consecutive pair of splice points into the curve-fitter. If the smoothed path from the previous step is fed into the curve-fitter, we get a spline like the following: Potrace is a popular bitmap tracing tool that also transforms bitmaps into vector graphics. Being able to produce high-quality output for low-resolution images, Potrace traces images by finding global optimal under certain assumptions. VTracer favours fidelity over simplification. Potrace and VTracer produce different results especially on small objects. Illustration of how assumptions affect tracing.
+ Potrace finds the global optimal way of tracing a shape, meaning it approximates parts using information from the entire shape, which leads to performance issues in high-resolution images. On the other hand, VTracer runs a linear algorithm on each cluster and has lower CPU and memory usage. In fact, we regularly use VTracer to process high resolution scans at 10 megapixels. Comparing to Adobe Illustrator's Live Trace, VTracer's output is much more compact thanks to a stacking strategy. VTracer's preprocessing stage utilizes Vision Cortex's image tree algorithm and never produces shapes with holes.Impression
Premise
The Clustering Algorithm
Stage 1: clustering
Stage 2: hierarchical clustering
Stage 3: tree walking
Simplification
Shape Simplification
Left to right: gradual reduction of shape details
Fidelity
Left: low fidelity; Right: high fidelity
Color Levels
Left: less color levels; Right: more color levels
Segmentation
Left: original; Mid: initial segmentation; Right: after aggregation
The Implementation
Processor
trait is the interface that future additions of algorithms will conform to. It also allows us to support video streaming applications in the future.Impressionism
Left: Artwork generated by Impression; Right: Water Lilies Painting by Claude Monet
Photo Credits
Vision Cortex - Semantic Computer Vision
Symbolic Barcode
Image Simplification
Image Vectorization
Optical Character Recognition
The Reversi Code
The first Amazon Go store, Downtown Seattle
ShapeSense - Shape Completion by Curve Stitching
A shape with a hole. There are 6 endpoints in this case.
An example of completed shape.
Simple Shape Completion
The first image is the ground truth (for reference only). The second image is the input to the ShapeCompletion pipeline.
Path Preprocessing
Yellow pixels denote the identified outline of the shape after simplification.
Tail Tangent Approximation
The naive approach is to simply take A as the tail tangent, but better (more practical/useful) approximations may be obtained by taking more subsequent segments into account (e.g. B, C, D).
Intrapolation
+If we considered the existing outline of the shape as separate curves at each endpoint, we would be doing interpolation between curves. However, in this project, we are focusing on curves (existing + missing) that form an outline of a single shape, so we argue that we are doing intrapolation within a curve.Both tail tangents at A and B point to the same side of the red, dashed line (the base).
+If the line originating from A and B intersect in the negative direction (as shown above), we simply correct them by bending them inwards to be perpendicular with the base.
A simple dot product operation can be used to detect such a scenario.
Color filling
Element Description Blank Background pixels. (Black in our demo) Structure Outline of the shape; The intrapolated curve(s) obtained above is rasterized and drawn onto the hole. (Blue in our demo) Texture Solid part of the shape; To be filled in this section. (Red in our demo) Complex Shape Completion
At a glance, we can tell how the endpoints should be grouped - A with B, and C with D, but how can we model the problem to match the endpoints such that the result of color filling always makes sense?
Endpoint Matching
Failed attempt: Local Proximity
The correct matching seems to be A with B and C with D, but the top two endpoints are the closest.
Avoiding Intersections
Robustness
Discussion
SymCode: The Symbolic Barcode for Humans and Machines
Forewords
Photo within an Amazon Go store
Background
Goals and Constraints
Design of symbols
Trace of shapes
The English alphabet
English alphabets
SymCode
a
,b
,c
,d
respectively, the three comparisons are defined as follows:Grid quadrants
(a+b)
vs (c+d)
(a+c)
vs (b+d)
(a+d)
vs (b+c)
LongRR symbol image
SmallDoubleLR symbol image
Trace of SymCode
Acute32 alphabet set
Acute32 trace counts
Side note:
efgh
For each comparison U vs V
efgh (top/bottom/left/right) grid
Advantages of traces
Scanning Pipeline
finder
, fitter
, recognizer
, decoder
.Stage 1: Locating Finder Candidates
Acute32
Stage 2: Fitting Perspective Transform
Left: image space; Right: object space
Acute32
Transform evaluation points and vectors: the 4 green vectors are v1 to v4
Stage 3: Recognizing Symbols
Rectify the image space
Recognition
Rectified image (code image in the correct object space)
Recognition process marked by bounding boxes
Stage 4: Decoding the SymCode
Acute32
SymCode
instance encodes 25 raw bits, where 20 bits are payload and 5 bits are CRC checksum.Conclusion
symcode
. For those who simply want to integrate SymCode into your application, please refer to the wasm distribution acute32
which is ready to use in any browser based Javascript projects.About Vision Cortex
VTracer
Path Walking
2 results of path-walking. Left: Naïve, Unoptimized walker. Right: Optimized walker.
Raw pixelated polygon
Path Simplification
Staircase Removal
Two examples of signed area. If outset(inset) is chosen, B(B’) is kept and B’(B) is discarded.
Simplify by Limiting penalties
Left: Path with wobbles. Right: Path approximated by red line, removing wobbles
Path Smoothing
Sample Simplified 'S' Shape
4-Point Scheme
4-Point Scheme performed on segment A₁A₂
If A2 is a corner, the 4-Point Scheme smooths it out iteratively.
Finding corners
+Angle difference from B to C is large => b is a cornerCorner-Preserving 4-Point Scheme
Length Thresholds 3.5/5.0/7.5
Iterations 0/1/2
Curve Fitting
Finding Splice Points
+Therefore, we cut (A..B) into (A..P) and (P..B).Angle Displacements shown along a path.
Final output
vs Potrace
Fidelity
+Left: Original input.
+
+Middle: Possible shape interpretation with the assumption that ambiguous “corners” in the original input are sharp corners.
+
Right: Possible shape interpretation with no assumptions: “corners” in the original image are represented by curves or round corners.Efficiency
vs Adobe Illustrator