Write a number from 0 to 9 using primaryPath and/or secondaryPath and see if this turtle can handle your handwriting.
If not recognized correctly, please add the paths to the comments and I will add it to the 'learning' set, although it is in no way (machine) learning.
This is a naive implementation that compares characters by the pixel after normalising and sampling paths. No data on how a path is drawn or direction of a path is used in the comparison.
(I added tertiaryPath in case you - for whatever reason - need a third path for a digit)
Log in to post a comment.
// Naive Decimal Digit OCR. Created by Jurgen Westerhof 2024 // The MIT License // // https://turtletoy.net/turtle/81b04ff7e6 // const display = 0; //min=0 max=2 step=1 (raw, normalized, grid) const primaryPath = 'M-1,-44 C-1,-50 25,-57 28,-54 C31,-51 37,-49 41,-44 C44,-40 44,-34 45,-29 C47,-15 45,6 39,15 C36,19 24,17 20,17 C20,17 11,17 12,18 C14,20 23,17 26,18 C38,20 46,27 52,38 C54,42 52,49 51,53 C50,57 50,64 48,66 C37,77 -5,76 -5,59'; // type=path, bbox=-70,-100,170,200 Click here to redraw the path const secondaryPath = 'M0,0';// type=path, bbox=-70,-100,170,200 Click here to redraw the path const tertiaryPath = 'M0,0';// type=path, bbox=-70,-100,170,200 Click here to redraw the path const turtle = new Turtle(); const text = new Text(); const grid = pixelize(primaryPath, secondaryPath, tertiaryPath, display); const defs = getCharacterDefinitions().flatMap((v, i) => v.map((p, ii) => [[i, ii], p])); const results = []; function walk(i) { if(i == defs.length) { const winner = results.sort((a, b) => a[1] < b[1]? 1: -1).shift()[0][0]; turtle.jump(-95, 85); text.print(turtle, '' + winner, 1); return false; } const test = defs[i]; while(test[1].length < 3) { test[1].push('M0,0'); } const testGrid = pixelize(...test[1]); const match = grid.reduce((a, c, x) => a + grid[x].reduce((a, c, i) => a + (c === testGrid[x][i]? 1: -1), 0), 0) / (grid.length * grid[0].length); results.push([test[0], Math.max(0, match)]); turtle.jump(-99, -97 + 5 * i); if(i < 33) { text.print(turtle, test[0][0] + '.' + (test[0][1] + 1) + ': ' + (Math.round(match * 10000)/10000), .13); } else if(i == 33) { text.print(turtle, '...', .13); } turtle.jump(-98 + 196 * i / defs.length, 99); turtle.goto(-98 + 196 * ((i+1) / defs.length), 99); return true; } function pixelize(primaryPath, secondaryPath, tertiaryPath, display = null) { //Although a new turtle, code in this function first was in walk(i) to do display things for one input character. But I now need to compare with more characters so I need to process more (predefined) paths and I didn't bother rewriting it properly get around the i-variable... so here it goes... ugliness const [paths, pathLengths, centerDelta, ratio] = getPathInfo(primaryPath, secondaryPath, tertiaryPath, display); const pts = []; for(let i = 0, max = pathLengths.reduce((a,c)=>a+c,0) + pathLengths.length - 1; i < max; i++) { const [pathIndex, step] = pathLengths.map(s => s + 1).reduce((a, c, idx) => a[2]? a: (a[1] < c? [idx, a[1], true]: [idx, a[1] - c, false]), [0, i, false]).slice(0,2); const raw = paths[pathIndex].p( step/pathLengths[pathIndex] ); const normalized = scale2(add2(centerDelta, raw), ratio); pts.push(normalized.map(c => c | 0)); if(display === 0 || display === 1) { if(step == 0) { turtle.jump(display == 0? raw: normalized); } else { turtle.goto(display == 0? raw: normalized); } } } //fill a grid slightly bigger than 85x100, so 95x110 with set or unset pixels by the path const gridSize = [95, 110]; const grid = Array.from({length: gridSize[0]}).map((v, col) => Array.from({length: gridSize[1]}).map((v, row) => pts.some(pt => col - 5 < pt[0] + 47 && pt[0] + 47 < col + 5 && row - 5 < pt[1] + 55 && pt[1] + 55 < row + 5) )); //console.log(grid); if(display == 2) { grid.forEach((v, col) => v.forEach((v, row) => { if(v) { turtle.jump(col - 47, row - 55 - .4); turtle.circle(.4); } })); } return grid; } function getPathInfo(primaryPath, secondaryPath, tertiaryPath, display) { //Although a new turtle, code in this function first was in in global scope to have walk(i) use it... so here it goes... ugliness with return variables const paths = [primaryPath, secondaryPath, tertiaryPath].filter(p => p != 'M0,0').map(p => Path(p.match(/([0-9.-]+|[MLC])/g))); const bb = paths.map(p => p.bb()).reduce((a, c) => [[Math.min(a[0][0], c[0][0]), Math.min(a[0][1], c[0][1])], [Math.max(a[1][0], c[1][0]), Math.max(a[1][1], c[1][1])]] , [[Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]]); //constrain everything to a 85x100 box const ratio = Math.min(85 / (bb[1][0] - bb[0][0]), 100 / (bb[1][1] - bb[0][1])); const centerDelta = scale2(lerp2(bb[0], bb[1], .5), -1); const normalizedPaths = [primaryPath, secondaryPath, tertiaryPath].filter(p => p != 'M0,0').map(p => Path( scaleTokens(p.match(/([0-9.-]+|[MLC])/g), ratio))); const pathLengths = normalizedPaths.map(p => p.length() | 0); return [paths, pathLengths, centerDelta, ratio]; } function scaleTokens(a, s) { return a.map((token, index) => { if (isNumber(token)) { return token * s; } else return token; }); } function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); } function add2(a, b) { return [a[0]+b[0], a[1]+b[1]]; } function scale2(a, s) { return [a[0]*s,a[1]*s]; } function lerp2(a,b,t) { return [a[0]*(1-t) + b[0]*t, a[1]*(1-t) + b[1]*t]; } //////////////////////////////////////////////////////////////// // Modified path utility code. Created by Reinder Nijhoff 2023 // Parses a single SVG path (only M, C and L statements are // supported). The p-method will return // [...position, ...derivative] for a normalized point t. // // https://turtletoy.net/turtle/46adb0ad70 // // Modified by Jurgen Westerhof 2024, added bb() and size() //////////////////////////////////////////////////////////////// function Path(tokens) { class MoveTo { constructor(p) { this.p0 = p; } p(t, s) { return [...this.p0, 1, 0]; } length() { return 0; } } class LineTo { constructor(p0, p1) { this.p0 = p0, this.p1 = p1; } p(t, s = 1) { const nt = 1 - t, p0 = this.p0, p1 = this.p1; return [ nt*p0[0] + t*p1[0], nt*p0[1] + t*p1[1], (p1[0] - p0[0]) * s, (p1[1] - p0[1]) * s, ]; } length() { const p0 = this.p0, p1 = this.p1; return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]); } } class BezierTo { constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; } p(t, s = 1) { const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1; return [ nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0], nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1], (3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s, (3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s, ]; } length() { return this._length || ( this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce( (a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0)); } } class Path { constructor(tokens) { this.segments = []; this.parsePath(tokens); } parsePath(t) { for (let s, i=0; i<t.length;) { switch (t[i++]) { case 'M': this.add(new MoveTo(s=[t[i++],t[i++]])); break; case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]])); break; case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]])); break; default: i++; } } } add(segment) { this.segments.push(segment); this._length = 0; this._bb = undefined; this._size = undefined; } length() { return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0)); } bb(sampleRate = .01) { if(this._bb === undefined) { this._bb = Array.from({length: 1 / sampleRate + 1}) .map((v, i) => this.p(i * sampleRate)) .reduce((p, c) => [[Math.min(p[0][0], c[0]), Math.min(p[0][1], c[1])],[Math.max(p[1][0], c[0]), Math.max(p[1][1], c[1])]], [[Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]]); } return this._bb; } size(sampleRate = .01) { if(this._size === undefined) { this._size = [this.bb(sampleRate)].map(v => [v[1][0] - v[0][0], v[1][1] - v[0][1]]).pop(); } return this._size; } p(t) { t = Math.max(Math.min(t, 1), 0) * this.length(); for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) { sl = this.segments[i].length(); if (t > l && t <= l + sl) { return this.segments[i].p((t-l)/sl, sl/this.length()); } } return this.segments[Math.min(1, this.segments.length-1)].p(0); } } return new Path(tokens); } function getCharacterDefinitions() { return [ [ //0 ['M2,-17 L1,-15 C-4,-10 -6,15 6,9 C14,5 20,-9 12,-17 C10,-19 4,-16 4,-15'], ['M10,-17 C2,-17 -8,-3 -1,4 L4,4 C20,4 21,-16 10,-16'], ['M9,-24 C-1,-24 -3,-2 1,6 C3,9 5,7 7,8 C8,9 12,10 14,9 C16,8 19,8 20,7 C27,0 20,-24 11,-24'], ], [ //1 ['M11,-33 C11,-15 4,7 4,26'], ['M-9,-22 C1,-22 7,-45 11,-45 L11,-43 C11,-37 10,-31 9,-25 C7,-11 5,5 5,19 C5,21 4,31 6,31'], ['M1,-33 C1,-9 0,14 0,38'], ], [ //2 ['M-13,-9 C-12,-9 -5,-21 -1,-17 C7,-9 -5,-2 -8,1 L-12,6 C-12,6 -11,5 -10,5 C-6,5 -4,6 0,6 C1,6 7,6 5,6'], ['M-17,-16 C-17,-21 10,-27 13,-22 C18,-13 5,12 2,20 C1,23 -0,27 -3,28 L-5,29 L-2,29 C0,29 17,30 17,31'], ['M-15,-16 C-15,-18 -4,-25 -1,-22 C2,-19 -8,-10 -9,-9 L-11,-8 C-11,-8 1,-8 2,-8'], ], [ //3 ['M10,-15 C19,-15 11,-10 11,-10 C11,-10 16,-10 15,-8 C14,-7 10,-4 10,-7'], ['M-15,-40 C-10,-45 7,-44 13,-41 C14,-40 14,-35 14,-33 C14,-31 14,-24 13,-23 C9,-19 5,-16 0,-14 L-6,-13 C-6,-13 -3,-13 -2,-13 C3,-13 9,-10 14,-8 C20,-5 15,15 14,19 C13,22 8,20 5,21 C-1,23 -29,17 -29,10'], ['M-3,-28 C2,-33 13,-26 15,-24 C16,-23 14,-21 14,-21 C11,-18 7,-17 3,-19 L1,-19 L11,-16 C21,-14 27,-11 21,0 C20,2 -1,-2 -4,-2'], ['M-1,-44 C-1,-50 25,-57 28,-54 C31,-51 37,-49 41,-44 C44,-40 44,-34 45,-29 C47,-15 45,6 39,15 C36,19 24,17 20,17 C20,17 11,17 12,18 C14,20 23,17 26,18 C38,20 46,27 52,38 C54,42 52,49 51,53 C50,57 50,64 48,66 C37,77 -5,76 -5,59'] ], [ //4 [ 'M3,-24 C3,-20 -7,-2 -10,3 C-11,4 -15,10 -13,12 C-12,13 -3,12 -1,12 C12,12 24,11 37,11', 'M20,3 C20,19 19,34 19,50', ], ['M13,27 C13,14 13,-2 16,-14 C16,-16 20,-24 19,-25 C18,-26 13,-22 11,-21 C1,-16 -1,-4 -7,2 L-8,5 C-7,6 1,5 3,5 C13,5 23,6 32,6'], ], [ //5 [ 'M-2,-16 C-2,-12 -12,13 -11,14 C-10,15 -8,13 -7,13 C-3,13 0,13 4,13 C9,13 18,14 22,16 C26,18 34,17 36,21 C38,25 36,33 35,38 C32,48 13,66 1,60 C-3,58 -9,57 -13,56 C-14,56 -22,49 -18,49', 'M-2,-14 C6,-18 40,-16 52,-16', ], ['M34,-30 C27,-30 6,-31 1,-29 C-3,-27 -4,-10 -4,-4 L-5,-2 C-5,-2 11,-3 16,-3 C30,-3 33,7 29,20 C25,33 12,27 0,27 C-4,27 -14,26 -14,26'], [ 'M-17,-47 C-17,-45 -20,-35 -19,-34 C-16,-31 -8,-34 -5,-29 C-2,-24 -1,-16 0,-11 C0,-9 1,-3 -1,-1 C-3,1 -12,-1 -12,-4', 'M-17,-46 C-11,-46 -5,-46 1,-46' ], ], [ //6 ['M10,-23 C10,-21 5,-19 3,-17 C1,-15 -7,7 -2,12 C1,15 12,13 14,11 C19,6 10,-4 8,-6 C4,-10 -0,0 -1,0'], ['M15,-46 C7,-46 1,-35 -2,-28 C-9,-12 -10,4 -13,21 C-14,26 -18,40 -13,43 C-7,46 5,42 11,41 C17,40 23,41 29,39 C33,38 39,36 42,33 L42,29 C44,20 46,4 34,0 C30,-1 24,0 20,0 C16,0 0,1 0,3'], ], [ //7 ['M-9,-26 C-3,-26 9,-25 17,-24 C19,-24 24,-23 24,-23 C24,-23 16,-12 15,-10 C12,0 2,20 2,31'], [ 'M-8,-45 C5,-45 18,-45 31,-45 C33,-45 44,-46 45,-45 C47,-43 44,-34 44,-31 C44,-25 41,-19 39,-13 C36,-3 36,9 33,20 C31,26 27,32 26,38 C26,39 24,47 24,45', 'M9,0 C23,0 41,-2 53,-2' ], ], [ //8 ['M6,11 C-5,11 2,29 10,21 C14,17 4,10 2,8 C-1,5 6,-2 9,1 C13,5 7,11 7,13'], ['M31,-39 C16,-39 2,-39 -8,-29 C-9,-28 -15,-20 -12,-17 C-7,-12 5,0 11,6 C12,7 11,13 11,15 C11,25 -1,34 -12,29 C-16,27 -14,18 -14,15 C-14,-9 13,-5 26,-18 C32,-24 37,-32 30,-39 L29,-41'], ['M19,-51 C10,-51 4,-50 -2,-46 C-4,-45 -5,-38 -3,-36 C3,-32 26,-34 29,-28 C38,-10 19,4 0,-2 C-4,-3 0,-15 1,-18 C4,-29 20,-32 28,-40 C30,-42 24,-47 23,-47'], ], [ //9 ['M13,2 C13,-0 5,2 7,6 C9,11 13,4 13,4 C13,4 8,17 8,14'], ['M-2,30 C-2,25 9,17 13,13 C22,4 22,-12 26,-24 C27,-26 31,-37 28,-40 C19,-49 6,-42 1,-32 C-0,-29 -2,-16 2,-15 C6,-14 12,-16 15,-16'], ['M34,-49 C22,-49 4,-54 -5,-45 C-6,-44 -5,-35 -5,-33 C-5,-29 -5,-26 -3,-23 C-1,-20 5,-20 9,-20 C13,-20 21,-19 25,-20 C28,-21 28,-28 30,-30 L30,-32 C29,-34 30,-44 30,-44 L29,-29 C27,-12 27,10 27,27 C25,45 24,52 24,50'], ['M25,-44 C25,-47 10,-47 8,-47 C-6,-47 -26,-45 -30,-30 C-30,-28 -32,-24 -31,-22 C-29,-18 -22,-18 -18,-18 C-4,-18 9,-18 22,-26 C26,-28 29,-35 29,-39 C29,-40 29,-41 28,-41 L26,-28 C23,-12 23,4 23,20 C23,25 23,30 23,35 C23,36 23,43 23,41'], ] ]; } //////////////////////////////////////////////////////////////// // Text utility code. Created by Reinder Nijhoff 2019 // https://turtletoy.net/turtle/1713ddbe99 // Jurgen 2021: Fixed Text.print() to restore turtle._fullCircle //. if was in e.g. degrees mode (or any other) //////////////////////////////////////////////////////////////// function Text() {class Text {print (t, str, scale = 1, italic = 0, kerning = 1) {let fc = t._fullCircle;t.radians();let pos = [t.x(), t.y()], h = t.h(), o = pos;str.split('').map(c => {const i = c.charCodeAt(0) - 32;if (i < 0 ) {pos = o = this.rotAdd([0, 48*scale], o, h);} else if (i > 96 ) {pos = this.rotAdd([16*scale, 0], o, h);} else {const d = dat[i], lt = d[0]*scale, rt = d[1]*scale, paths = d[2];paths.map( p => {t.up();p.map( s=> {t.goto(this.rotAdd([(s[0]-s[1]*italic)*scale - lt, s[1]*scale], pos, h));t.down();});});pos = this.rotAdd([(rt - lt)*kerning, 0], pos, h);}});t._fullCircle = fc;}rotAdd (a, b, h) {return [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0], Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];}}const dat = ('br>eoj^jl<jqirjskrjq>brf^fe<n^ne>`ukZdz<qZjz<dgrg<cmqm>`thZhw<lZlw<qao_l^h^e_caccdeefggmiojpkqmqporlshsercp>^vs^as<f^h`hbgdeeceacaab_d^f^h_k`n`q_s^<olmmlolqnspsrrspsnqlol>]wtgtfsereqfphnmlpjrhsdsbraq`o`makbjifjekckaj_h^f_eaecffhimporqssstrtq>eoj`i_j^k_kajcid>cqnZl\\j_hcghglhqjulxnz>cqfZh\\j_lcmhmllqjuhxfz>brjdjp<egom<ogem>]wjajs<ajsj>fnkojpiojnkokqis>]wajsj>fnjniojpkojn>_usZaz>`ti^f_dbcgcjdofrisksnrpoqjqgpbn_k^i^>`tfbhak^ks>`tdcdbe`f_h^l^n_o`pbpdofmicsqs>`te^p^jfmfogphqkqmppnrkshserdqco>`tm^clrl<m^ms>`to^e^dgefhekenfphqkqmppnrkshserdqco>`tpao_l^j^g_ebdgdlepgrjsksnrppqmqlpingkfjfggeidl>`tq^gs<c^q^>`th^e_dadceegfkgnhpjqlqopqorlshserdqcocldjfhigmfoepcpao_l^h^>`tpeohmjjkikfjdhcecddaf_i^j^m_oapepjoomrjshserdp>fnjgihjikhjg<jniojpkojn>fnjgihjikhjg<kojpiojnkokqis>^vrabjrs>]wagsg<amsm>^vbarjbs>asdcdbe`f_h^l^n_o`pbpdofngjijl<jqirjskrjq>]xofndlcicgdfeehekfmhnknmmnk<icgefhfkgmhn<ocnknmpnrntluiugtdsbq`o_l^i^f_d`bbad`g`jambodqfrislsorqqrp<pcokompn>asj^bs<j^rs<elol>_tc^cs<c^l^o_p`qbqdpfoglh<chlhoipjqlqopqorlscs>`urcqao_m^i^g_eadccfckdnepgrismsorqprn>_tc^cs<c^j^m_oapcqfqkpnopmrjscs>`sd^ds<d^q^<dhlh<dsqs>`rd^ds<d^q^<dhlh>`urcqao_m^i^g_eadccfckdnepgrismsorqprnrk<mkrk>_uc^cs<q^qs<chqh>fnj^js>brn^nnmqlrjshsfreqdndl>_tc^cs<q^cl<hgqs>`qd^ds<dsps>^vb^bs<b^js<r^js<r^rs>_uc^cs<c^qs<q^qs>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^>_tc^cs<c^l^o_p`qbqepgohlici>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^<koqu>_tc^cs<c^l^o_p`qbqdpfoglhch<jhqs>`tqao_l^h^e_caccdeefggmiojpkqmqporlshsercp>brj^js<c^q^>_uc^cmdpfrisksnrppqmq^>asb^js<r^js>^v`^es<j^es<j^os<t^os>`tc^qs<q^cs>asb^jhjs<r^jh>`tq^cs<c^q^<csqs>cqgZgz<hZhz<gZnZ<gznz>cqc^qv>cqlZlz<mZmz<fZmZ<fzmz>brj\\bj<j\\rj>asazsz>fnkcieigjhkgjfig>atpeps<phnfleiegfehdkdmepgrislsnrpp>`sd^ds<dhffhekemfohpkpmopmrkshsfrdp>asphnfleiegfehdkdmepgrislsnrpp>atp^ps<phnfleiegfehdkdmepgrislsnrpp>asdkpkpiognfleiegfehdkdmepgrislsnrpp>eqo^m^k_jbjs<gene>atpepuoxnylzizgy<phnfleiegfehdkdmepgrislsnrpp>ate^es<eihfjemeofpips>fni^j_k^j]i^<jejs>eoj^k_l^k]j^<kekvjyhzfz>are^es<oeeo<ikps>fnj^js>[y_e_s<_ibfdegeifjijs<jimfoeretfuius>ateees<eihfjemeofpips>atiegfehdkdmepgrislsnrppqmqkphnfleie>`sdedz<dhffhekemfohpkpmopmrkshsfrdp>atpepz<phnfleiegfehdkdmepgrislsnrpp>cpgegs<gkhhjfleoe>bsphofleieffehfjhkmlompopporlsisfrep>eqj^jokrmsos<gene>ateeeofrhsksmrpo<peps>brdejs<pejs>_ubefs<jefs<jens<rens>bseeps<pees>brdejs<pejshwfydzcz>bspees<eepe<esps>cqlZj[i\\h^h`ibjckekgii<j[i]i_jakbldlfkhgjkllnlpkrjsiuiwjy<ikkmkojqirhthvixjylz>fnjZjz>cqhZj[k\\l^l`kbjcieigki<j[k]k_jaibhdhfihmjilhnhpirjskukwjy<kkimiojqkrltlvkxjyhz>^vamakbhdgfghhlknlplrksi<akbidhfhhillnmpmrlsisg>brb^bscsc^d^dsese^f^fsgsg^h^hsisi^j^jsksk^l^lsmsm^n^nsoso^p^psqsq^r^rs').split('>').map(r=> { return [r.charCodeAt(0)-106,r.charCodeAt(1)-106, r.substr(2).split('<').map(a => {const ret = []; for (let i=0; i<a.length; i+=2) {ret.push(a.substr(i, 2).split('').map(b => b.charCodeAt(0)-106));} return ret; })]; });return new Text();}