glyphbyte 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # glyphbyte
2
+
3
+ One drawn glyph is one byte. This package is the dependency-free JavaScript implementation
4
+ of [GlyphByte](https://glyphbyte.dev): read a hand-drawn row of glyphs from a photo on the
5
+ device, render rows to draw from, and resolve the bytes on nostr as event id and pubkey
6
+ prefixes. The specification with test vectors is in the repository under `spec/`.
7
+
8
+ ```js
9
+ import { readImage, toGray, loadBundledModel, drawRow, loadShapes, lookup } from 'glyphbyte';
10
+
11
+ const model = await loadBundledModel(); // Node; in a browser pass weights to loadWeights()
12
+ const gray = toGray(imageData.data, width, height); // from a canvas
13
+ const result = readImage(gray, width, height, model); // { best, sequences, reads, warnings, detection }
14
+
15
+ drawRow(canvas, await loadShapes(), [0xe8, 0xed, 0x37, 0x98, 0xc6, 0xff]); // a row to copy from
16
+ const hits = await lookup(result.sequences.map(s => s.hex), { relay: 'wss://wheat.oslim.dev' });
17
+ ```
18
+
19
+ The same code, bundled into one HTML file, is the GlyphByte napplet (NIP-5D).
20
+ MIT.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "glyphbyte",
3
+ "version": "0.1.0",
4
+ "description": "One drawn glyph is one byte. Read hand-drawn glyph rows from photos on the device, render rows to draw from, and resolve the bytes on nostr as event id and pubkey prefixes.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "!src/app.js",
13
+ "weights.bin",
14
+ "weights.bin.json",
15
+ "shapes.json",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "python build.py weights.bin weights.bin.json",
20
+ "test": "node test/nip19_check.mjs"
21
+ },
22
+ "keywords": [
23
+ "nostr",
24
+ "glyph",
25
+ "handwriting",
26
+ "ocr",
27
+ "napplet",
28
+ "nip-19",
29
+ "prefix"
30
+ ],
31
+ "homepage": "https://glyphbyte.dev",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/arkin0x/glyphbyte.git",
35
+ "directory": "napplet"
36
+ },
37
+ "license": "MIT",
38
+ "author": "arkinox",
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "sideEffects": false
43
+ }
package/shapes.json ADDED
@@ -0,0 +1 @@
1
+ [{"name": "house", "outer": [[0.01579, -0.47368], [0.00078, -0.45867], [-0.01423, -0.44366], [-0.03232, -0.4361], [-0.04733, -0.42109], [-0.06234, -0.40608], [-0.08044, -0.39851], [-0.09545, -0.3835], [-0.11046, -0.36849], [-0.12855, -0.36093], [-0.14356, -0.34592], [-0.15857, -0.33091], [-0.17666, -0.32334], [-0.19167, -0.30833], [-0.20727, -0.29474], [-0.22477, -0.28575], [-0.23978, -0.27074], [-0.25479, -0.25573], [-0.27289, -0.24817], [-0.2879, -0.23316], [-0.30411, -0.22105], [-0.321, -0.21058], [-0.33601, -0.19557], [-0.35102, -0.18056], [-0.36911, -0.17299], [-0.38412, -0.15798], [-0.40095, -0.14737], [-0.41722, -0.13541], [-0.43223, -0.1204], [-0.44724, -0.10539], [-0.46406, -0.09474], [-0.48343, -0.09026], [-0.5, -0.08206], [-0.5, -0.06083], [-0.5, -0.0396], [-0.5, -0.01838], [-0.5, 0.00285], [-0.5, 0.02408], [-0.5, 0.0453], [-0.5, 0.06653], [-0.5, 0.08776], [-0.5, 0.10899], [-0.5, 0.13021], [-0.5, 0.15144], [-0.5, 0.17267], [-0.5, 0.19389], [-0.5, 0.21512], [-0.5, 0.23635], [-0.5, 0.25758], [-0.5, 0.2788], [-0.5, 0.30003], [-0.5, 0.32126], [-0.5, 0.34248], [-0.5, 0.36371], [-0.5, 0.38494], [-0.5, 0.40616], [-0.5, 0.42739], [-0.49349, 0.44211], [-0.47662, 0.45263], [-0.45613, 0.4544], [-0.43853, 0.46316], [-0.4173, 0.46316], [-0.39607, 0.46316], [-0.37484, 0.46316], [-0.35362, 0.46316], [-0.33239, 0.46316], [-0.31116, 0.46316], [-0.28994, 0.46316], [-0.26871, 0.46316], [-0.24748, 0.46316], [-0.22625, 0.46316], [-0.20503, 0.46316], [-0.1838, 0.46316], [-0.16257, 0.46316], [-0.14135, 0.46316], [-0.12012, 0.46316], [-0.09889, 0.46316], [-0.07766, 0.46316], [-0.05644, 0.46316], [-0.03521, 0.46316], [-0.01398, 0.46316], [0.00724, 0.46316], [0.02847, 0.46316], [0.0497, 0.46316], [0.07092, 0.46316], [0.09215, 0.46316], [0.11338, 0.46316], [0.13461, 0.46316], [0.15583, 0.46316], [0.17706, 0.46316], [0.19829, 0.46316], [0.21951, 0.46316], [0.24074, 0.46316], [0.26197, 0.46316], [0.2832, 0.46316], [0.30442, 0.46316], [0.32565, 0.46316], [0.34688, 0.46316], [0.3681, 0.46316], [0.38933, 0.46316], [0.41056, 0.46316], [0.43179, 0.46316], [0.45301, 0.46316], [0.47424, 0.46316], [0.48947, 0.45716], [0.48947, 0.43594], [0.48947, 0.41471], [0.48947, 0.39348], [0.48947, 0.37226], [0.48947, 0.35103], [0.48947, 0.3298], [0.48947, 0.30858], [0.48947, 0.28735], [0.48947, 0.26612], [0.48947, 0.24489], [0.48947, 0.22367], [0.48947, 0.20244], [0.48947, 0.18121], [0.48947, 0.15999], [0.48947, 0.13876], [0.48947, 0.11753], [0.48947, 0.0963], [0.48947, 0.07508], [0.48947, 0.05385], [0.48947, 0.03262], [0.48947, 0.0114], [0.48947, -0.00983], [0.48947, -0.03106], [0.48947, -0.05229], [0.48947, -0.07351], [0.48947, -0.09474], [0.48191, -0.11283], [0.4669, -0.12784], [0.4494, -0.13684], [0.43379, -0.15042], [0.41878, -0.16543], [0.40316, -0.17895], [0.38568, -0.188], [0.37067, -0.20301], [0.35258, -0.21058], [0.33757, -0.22559], [0.32256, -0.2406], [0.30631, -0.25263], [0.28945, -0.26318], [0.27445, -0.27819], [0.25635, -0.28575], [0.24134, -0.30076], [0.22633, -0.31577], [0.20947, -0.32632], [0.19323, -0.33835], [0.17822, -0.35336], [0.16013, -0.36093], [0.14512, -0.37594], [0.13011, -0.39095], [0.11263, -0.4], [0.097, -0.41352], [0.08199, -0.42853], [0.0639, -0.4361], [0.04889, -0.45111], [0.03388, -0.46612]], "features": []}, {"name": "chevron", "outer": [[-0.01111, -0.5], [-0.02792, -0.48319], [-0.04485, -0.46667], [-0.0648, -0.45743], [-0.08161, -0.44062], [-0.10167, -0.43166], [-0.11848, -0.41485], [-0.13529, -0.39804], [-0.15527, -0.38889], [-0.17217, -0.37228], [-0.18901, -0.35556], [-0.20904, -0.34652], [-0.22585, -0.32971], [-0.24266, -0.3129], [-0.26272, -0.30394], [-0.27954, -0.28713], [-0.29943, -0.27778], [-0.31641, -0.26137], [-0.33322, -0.24456], [-0.35234, -0.23333], [-0.37009, -0.21879], [-0.3869, -0.20198], [-0.40525, -0.18889], [-0.42378, -0.17622], [-0.44059, -0.15941], [-0.46065, -0.15046], [-0.47746, -0.13365], [-0.49427, -0.11684], [-0.5, -0.09544], [-0.5, -0.07166], [-0.5, -0.04789], [-0.5, -0.02412], [-0.5, -0.00034], [-0.5, 0.02343], [-0.5, 0.0472], [-0.5, 0.07098], [-0.5, 0.09475], [-0.5, 0.11852], [-0.5, 0.1423], [-0.5, 0.16607], [-0.5, 0.18984], [-0.5, 0.21362], [-0.5, 0.23739], [-0.5, 0.26116], [-0.5, 0.28493], [-0.5, 0.30871], [-0.5, 0.33248], [-0.5, 0.35625], [-0.5, 0.38003], [-0.5, 0.4038], [-0.5, 0.42757], [-0.5, 0.45135], [-0.5, 0.47512], [-0.49293, 0.48182], [-0.47612, 0.465], [-0.45931, 0.44819], [-0.43924, 0.43924], [-0.42243, 0.42243], [-0.40335, 0.41111], [-0.38556, 0.39667], [-0.36875, 0.37986], [-0.35044, 0.36667], [-0.33187, 0.35409], [-0.31506, 0.33728], [-0.295, 0.32833], [-0.27819, 0.31152], [-0.26138, 0.29471], [-0.24131, 0.28576], [-0.2245, 0.26895], [-0.20769, 0.25214], [-0.18763, 0.24318], [-0.17082, 0.22637], [-0.15337, 0.21111], [-0.13394, 0.20061], [-0.11713, 0.1838], [-0.10032, 0.16699], [-0.08026, 0.15804], [-0.06345, 0.14123], [-0.04294, 0.13333], [-0.02332, 0.12332], [0.0, 0.12222], [0.01917, 0.13333], [0.04013, 0.14013], [0.06019, 0.14908], [0.077, 0.16589], [0.09381, 0.1827], [0.11388, 0.19166], [0.13069, 0.20847], [0.14876, 0.22222], [0.16756, 0.23423], [0.18437, 0.25104], [0.20167, 0.26667], [0.22125, 0.2768], [0.23806, 0.29361], [0.25487, 0.31042], [0.27493, 0.31938], [0.29174, 0.33619], [0.30855, 0.353], [0.32862, 0.36195], [0.34543, 0.37876], [0.36501, 0.38889], [0.3823, 0.40452], [0.39911, 0.42133], [0.41792, 0.43333], [0.43599, 0.4471], [0.4528, 0.46391], [0.47286, 0.47286], [0.48889, 0.48778], [0.48889, 0.46401], [0.48889, 0.44024], [0.48889, 0.41646], [0.48889, 0.39269], [0.48889, 0.36892], [0.48889, 0.34514], [0.48889, 0.32137], [0.48889, 0.2976], [0.48889, 0.27382], [0.48889, 0.25005], [0.48889, 0.22628], [0.48889, 0.2025], [0.48889, 0.17873], [0.48889, 0.15496], [0.48889, 0.13118], [0.48889, 0.10741], [0.48889, 0.08364], [0.48889, 0.05987], [0.48889, 0.03609], [0.48889, 0.01232], [0.48889, -0.01145], [0.48889, -0.03523], [0.48889, -0.059], [0.48889, -0.08277], [0.48889, -0.10655], [0.47531, -0.12469], [0.45524, -0.13365], [0.43843, -0.15046], [0.42137, -0.16667], [0.40156, -0.17622], [0.38475, -0.19303], [0.36794, -0.20984], [0.34787, -0.21879], [0.33106, -0.2356], [0.311, -0.24456], [0.29419, -0.26137], [0.27738, -0.27818], [0.26057, -0.29499], [0.2405, -0.30394], [0.22369, -0.32075], [0.20513, -0.33333], [0.18682, -0.34652], [0.17001, -0.36333], [0.15222, -0.37778], [0.13313, -0.38909], [0.11632, -0.4059], [0.09626, -0.41485], [0.07945, -0.43166], [0.06264, -0.44847], [0.04257, -0.45743], [0.02576, -0.47424], [0.00806, -0.48889]], "features": []}, {"name": "bookmark", "outer": [[-0.44086, -0.5], [-0.44388, -0.47548], [-0.4621, -0.45725], [-0.46512, -0.43273], [-0.48334, -0.41451], [-0.49462, -0.39341], [-0.49462, -0.36764], [-0.49462, -0.34187], [-0.49462, -0.3161], [-0.49462, -0.29033], [-0.49462, -0.26455], [-0.49462, -0.23878], [-0.49462, -0.21301], [-0.49462, -0.18724], [-0.49462, -0.16147], [-0.49462, -0.1357], [-0.49462, -0.10993], [-0.49462, -0.08415], [-0.49462, -0.05838], [-0.49462, -0.03261], [-0.49462, -0.00684], [-0.49462, 0.01893], [-0.49462, 0.0447], [-0.49462, 0.07048], [-0.49462, 0.09625], [-0.49462, 0.12202], [-0.49462, 0.14779], [-0.49462, 0.17356], [-0.49462, 0.19933], [-0.49462, 0.2251], [-0.49462, 0.25088], [-0.49462, 0.27665], [-0.49462, 0.30242], [-0.49462, 0.32819], [-0.49462, 0.35396], [-0.49462, 0.37973], [-0.48387, 0.40105], [-0.47479, 0.42306], [-0.46237, 0.44369], [-0.46237, 0.46946], [-0.44738, 0.47426], [-0.42877, 0.45699], [-0.40745, 0.44624], [-0.38326, 0.4424], [-0.36481, 0.42473], [-0.34367, 0.41356], [-0.32545, 0.39534], [-0.30407, 0.38472], [-0.28585, 0.3665], [-0.26713, 0.34946], [-0.24625, 0.33765], [-0.22803, 0.31943], [-0.20763, 0.30645], [-0.18844, 0.29059], [-0.17021, 0.27236], [-0.14884, 0.26174], [-0.13062, 0.24352], [-0.10995, 0.23118], [-0.09102, 0.21468], [-0.0728, 0.19645], [-0.05142, 0.18583], [-0.0332, 0.16761], [-0.01227, 0.15591], [0.00639, 0.15156], [0.02462, 0.16978], [0.04599, 0.1804], [0.06421, 0.19862], [0.08541, 0.20968], [0.10381, 0.22747], [0.12203, 0.24569], [0.14341, 0.25631], [0.16163, 0.27453], [0.183, 0.28515], [0.20122, 0.30337], [0.21945, 0.3216], [0.24082, 0.33222], [0.25904, 0.35044], [0.27727, 0.36866], [0.29864, 0.37928], [0.31686, 0.39751], [0.33823, 0.40813], [0.35646, 0.42635], [0.37468, 0.44457], [0.3992, 0.44759], [0.42108, 0.45699], [0.44195, 0.46883], [0.46017, 0.48705], [0.47312, 0.47733], [0.47312, 0.45156], [0.47312, 0.42579], [0.48387, 0.40447], [0.48387, 0.3787], [0.48387, 0.35293], [0.48387, 0.32716], [0.48387, 0.30138], [0.48387, 0.27561], [0.48387, 0.24984], [0.48387, 0.22407], [0.48387, 0.1983], [0.48387, 0.17253], [0.48387, 0.14676], [0.48387, 0.12098], [0.48387, 0.09521], [0.48387, 0.06944], [0.48387, 0.04367], [0.48387, 0.0179], [0.48387, -0.00787], [0.48387, -0.03365], [0.48387, -0.05942], [0.48387, -0.08519], [0.48387, -0.11096], [0.48387, -0.13673], [0.48387, -0.1625], [0.48387, -0.18827], [0.48387, -0.21405], [0.48387, -0.23982], [0.48387, -0.26559], [0.48387, -0.29136], [0.48387, -0.31713], [0.48387, -0.3429], [0.48387, -0.36868], [0.48387, -0.39445], [0.47946, -0.41839], [0.47312, -0.44154], [0.45822, -0.46114], [0.44086, -0.47972], [0.43537, -0.5], [0.4096, -0.5], [0.38383, -0.5], [0.35806, -0.5], [0.33229, -0.5], [0.30651, -0.5], [0.28074, -0.5], [0.25497, -0.5], [0.2292, -0.5], [0.20343, -0.5], [0.17766, -0.5], [0.15188, -0.5], [0.12611, -0.5], [0.10034, -0.5], [0.07457, -0.5], [0.0488, -0.5], [0.02303, -0.5], [-0.00274, -0.5], [-0.02852, -0.5], [-0.05429, -0.5], [-0.08006, -0.5], [-0.10583, -0.5], [-0.1316, -0.5], [-0.15737, -0.5], [-0.18315, -0.5], [-0.20892, -0.5], [-0.23469, -0.5], [-0.26046, -0.5], [-0.28623, -0.5], [-0.312, -0.5], [-0.33777, -0.5], [-0.36355, -0.5], [-0.38932, -0.5], [-0.41509, -0.5]], "features": []}, {"name": "crown", "outer": [[-0.01111, -0.48889], [-0.02501, -0.46387], [-0.03892, -0.43886], [-0.05282, -0.41385], [-0.07458, -0.39209], [-0.08848, -0.36707], [-0.10239, -0.34206], [-0.11629, -0.31704], [-0.13019, -0.29203], [-0.1441, -0.26701], [-0.158, -0.242], [-0.1719, -0.21698], [-0.18581, -0.19197], [-0.19971, -0.16696], [-0.21111, -0.1409], [-0.22752, -0.11693], [-0.25579, -0.11088], [-0.2808, -0.10302], [-0.30822, -0.11111], [-0.33083, -0.13083], [-0.34444, -0.15596], [-0.35556, -0.18214], [-0.36667, -0.20831], [-0.37859, -0.23414], [-0.4, -0.25605], [-0.41111, -0.28222], [-0.42222, -0.30839], [-0.43333, -0.33456], [-0.44444, -0.36073], [-0.45556, -0.3869], [-0.46805, -0.4125], [-0.47778, -0.43924], [-0.48889, -0.46541], [-0.48889, -0.43715], [-0.48889, -0.40637], [-0.48889, -0.3756], [-0.48889, -0.34483], [-0.48889, -0.31405], [-0.48889, -0.28328], [-0.49105, -0.2534], [-0.5, -0.22633], [-0.5, -0.19556], [-0.5, -0.16479], [-0.5, -0.13401], [-0.5, -0.10324], [-0.5, -0.07247], [-0.5, -0.04169], [-0.5, -0.01092], [-0.5, 0.01985], [-0.5, 0.05063], [-0.5, 0.0814], [-0.5, 0.11217], [-0.5, 0.14295], [-0.5, 0.17372], [-0.5, 0.20449], [-0.5, 0.23527], [-0.5, 0.26604], [-0.5, 0.29681], [-0.5, 0.32759], [-0.5, 0.35836], [-0.5, 0.38913], [-0.5, 0.41991], [-0.49559, 0.44885], [-0.48889, 0.47685], [-0.45904, 0.47778], [-0.42827, 0.47778], [-0.39749, 0.47778], [-0.36672, 0.47778], [-0.33595, 0.47778], [-0.30517, 0.47778], [-0.2744, 0.47778], [-0.24363, 0.47778], [-0.21285, 0.47778], [-0.18208, 0.47778], [-0.15131, 0.47778], [-0.12053, 0.47778], [-0.08976, 0.47778], [-0.05899, 0.47778], [-0.02821, 0.47778], [0.00256, 0.47778], [0.03333, 0.47778], [0.06411, 0.47778], [0.09488, 0.47778], [0.12565, 0.47778], [0.15643, 0.47778], [0.1872, 0.47778], [0.21797, 0.47778], [0.24875, 0.47778], [0.27952, 0.47778], [0.31029, 0.47778], [0.34107, 0.47778], [0.37184, 0.47778], [0.40261, 0.47778], [0.43339, 0.47778], [0.46416, 0.47778], [0.48889, 0.47173], [0.48889, 0.44096], [0.48889, 0.41019], [0.48889, 0.37941], [0.48889, 0.34864], [0.48889, 0.31787], [0.48889, 0.28709], [0.48889, 0.25632], [0.48889, 0.22554], [0.48889, 0.19477], [0.48889, 0.164], [0.48889, 0.13322], [0.48889, 0.10245], [0.48889, 0.07168], [0.48889, 0.0409], [0.48889, 0.01013], [0.48889, -0.02064], [0.48889, -0.05142], [0.48889, -0.08219], [0.48889, -0.11296], [0.48889, -0.14374], [0.48889, -0.17451], [0.48889, -0.20528], [0.48889, -0.23606], [0.48889, -0.26683], [0.48889, -0.2976], [0.48889, -0.32838], [0.48889, -0.35915], [0.48889, -0.38992], [0.48889, -0.4207], [0.48889, -0.45147], [0.48889, -0.48224], [0.47968, -0.46857], [0.47364, -0.4403], [0.45973, -0.41529], [0.44583, -0.39028], [0.43333, -0.36468], [0.42588, -0.33699], [0.41198, -0.31198], [0.4, -0.28617], [0.38889, -0.25999], [0.37027, -0.23693], [0.35636, -0.21192], [0.34444, -0.18608], [0.33641, -0.15864], [0.32251, -0.13362], [0.30861, -0.10861], [0.2814, -0.1], [0.25858, -0.09698], [0.23031, -0.10302], [0.21111, -0.12584], [0.2, -0.15201], [0.18074, -0.17481], [0.16684, -0.19983], [0.15556, -0.22593], [0.13903, -0.24986], [0.12513, -0.27487], [0.11123, -0.29988], [0.1, -0.32601], [0.08889, -0.35218], [0.06952, -0.37493], [0.05561, -0.39994], [0.03385, -0.4217], [0.02222, -0.44766], [0.01111, -0.47383]], "features": []}, {"name": "drop", "outer": [[-0.01124, -0.5], [-0.01574, -0.48426], [-0.02819, -0.47181], [-0.04064, -0.45936], [-0.04515, -0.44362], [-0.0576, -0.43117], [-0.07005, -0.41871], [-0.0825, -0.40626], [-0.09495, -0.39381], [-0.10112, -0.37876], [-0.11191, -0.36562], [-0.12436, -0.35317], [-0.13483, -0.3399], [-0.14131, -0.32498], [-0.15376, -0.31253], [-0.16621, -0.30008], [-0.17867, -0.28763], [-0.19101, -0.27513], [-0.19562, -0.25943], [-0.20807, -0.24698], [-0.22052, -0.23453], [-0.23297, -0.22208], [-0.24542, -0.20963], [-0.25787, -0.19718], [-0.26238, -0.18144], [-0.27483, -0.16899], [-0.28728, -0.15654], [-0.29973, -0.14409], [-0.31218, -0.13164], [-0.31669, -0.1159], [-0.32914, -0.10345], [-0.33708, -0.08913], [-0.34609, -0.07525], [-0.35855, -0.0628], [-0.36305, -0.04706], [-0.37079, -0.03266], [-0.38001, -0.01887], [-0.38202, -0.0021], [-0.38202, 0.01551], [-0.39326, 0.02847], [-0.39326, 0.04607], [-0.39326, 0.06368], [-0.39326, 0.08129], [-0.39326, 0.0989], [-0.39326, 0.1165], [-0.39326, 0.13411], [-0.39326, 0.15172], [-0.39326, 0.16933], [-0.38422, 0.18319], [-0.38202, 0.19989], [-0.38202, 0.2175], [-0.37079, 0.23045], [-0.37079, 0.24806], [-0.3617, 0.2619], [-0.35719, 0.27764], [-0.34831, 0.29157], [-0.34023, 0.30583], [-0.32778, 0.31828], [-0.32328, 0.33403], [-0.31083, 0.34648], [-0.29838, 0.35893], [-0.28593, 0.37138], [-0.27347, 0.38383], [-0.26102, 0.39628], [-0.24857, 0.40873], [-0.23283, 0.41324], [-0.22038, 0.42569], [-0.20563, 0.43258], [-0.19219, 0.44264], [-0.17645, 0.44715], [-0.16212, 0.45506], [-0.14826, 0.4641], [-0.13251, 0.46861], [-0.1186, 0.47753], [-0.10099, 0.47753], [-0.08338, 0.47753], [-0.06955, 0.48663], [-0.05282, 0.48876], [-0.03522, 0.48876], [-0.01761, 0.48876], [0.0, 0.48876], [0.01761, 0.48876], [0.03522, 0.48876], [0.05282, 0.48876], [0.06626, 0.47869], [0.08338, 0.47753], [0.10099, 0.47753], [0.11677, 0.47312], [0.13155, 0.46629], [0.14496, 0.45616], [0.16071, 0.45165], [0.17507, 0.44382], [0.1889, 0.4347], [0.20464, 0.43019], [0.21709, 0.41774], [0.23154, 0.41011], [0.24528, 0.40078], [0.25773, 0.38833], [0.27018, 0.37588], [0.28263, 0.36343], [0.29508, 0.35098], [0.30754, 0.33853], [0.31461, 0.32385], [0.32449, 0.31034], [0.33694, 0.29789], [0.34145, 0.28215], [0.34831, 0.26738], [0.3584, 0.25396], [0.35955, 0.23682], [0.36742, 0.22247], [0.37079, 0.20626], [0.37079, 0.18865], [0.38093, 0.17525], [0.38202, 0.15809], [0.38202, 0.14048], [0.38202, 0.12288], [0.38202, 0.10527], [0.38202, 0.08766], [0.38202, 0.07005], [0.38202, 0.05244], [0.38202, 0.03484], [0.37434, 0.02041], [0.37079, 0.00428], [0.36533, -0.01107], [0.35955, -0.02629], [0.35632, -0.04256], [0.34831, -0.05685], [0.33936, -0.07075], [0.32691, -0.0832], [0.32241, -0.09894], [0.30996, -0.11139], [0.30337, -0.12627], [0.293, -0.13958], [0.28055, -0.15203], [0.2681, -0.16448], [0.25565, -0.17693], [0.24719, -0.19104], [0.23869, -0.20513], [0.22624, -0.21758], [0.21379, -0.23003], [0.20134, -0.24248], [0.18889, -0.25493], [0.17978, -0.26876], [0.17194, -0.28312], [0.15948, -0.29557], [0.14703, -0.30802], [0.13458, -0.32047], [0.1236, -0.33353], [0.11763, -0.34867], [0.10518, -0.36112], [0.09273, -0.37357], [0.08028, -0.38602], [0.07577, -0.40176], [0.06332, -0.41421], [0.05087, -0.42666], [0.03842, -0.43911], [0.02597, -0.45156], [0.02146, -0.4673], [0.00901, -0.47975], [0.0, -0.49363]], "features": []}, {"name": "tee", "outer": [[-0.5, -0.5], [-0.5, -0.47568], [-0.5, -0.45137], [-0.5, -0.42705], [-0.5, -0.40274], [-0.5, -0.37842], [-0.5, -0.35411], [-0.5, -0.32979], [-0.5, -0.30548], [-0.5, -0.28116], [-0.5, -0.25685], [-0.5, -0.23253], [-0.5, -0.20821], [-0.5, -0.1839], [-0.48181, -0.17778], [-0.45749, -0.17778], [-0.43318, -0.17778], [-0.40886, -0.17778], [-0.38454, -0.17778], [-0.36023, -0.17778], [-0.33591, -0.17778], [-0.3116, -0.17778], [-0.28728, -0.17778], [-0.26297, -0.17778], [-0.24035, -0.17368], [-0.21894, -0.16667], [-0.19945, -0.15501], [-0.18226, -0.13781], [-0.16667, -0.11996], [-0.16667, -0.09564], [-0.16667, -0.07133], [-0.16667, -0.04701], [-0.16667, -0.0227], [-0.16667, 0.00162], [-0.16667, 0.02593], [-0.16667, 0.05025], [-0.16667, 0.07457], [-0.16667, 0.09888], [-0.16667, 0.1232], [-0.16667, 0.14751], [-0.16667, 0.17183], [-0.16667, 0.19614], [-0.16667, 0.22046], [-0.16667, 0.24477], [-0.16667, 0.26909], [-0.16667, 0.2934], [-0.16667, 0.31772], [-0.16667, 0.34204], [-0.16667, 0.36635], [-0.16667, 0.39067], [-0.16667, 0.41498], [-0.16667, 0.4393], [-0.16667, 0.46361], [-0.16667, 0.48793], [-0.14331, 0.48889], [-0.119, 0.48889], [-0.09468, 0.48889], [-0.07037, 0.48889], [-0.04605, 0.48889], [-0.02174, 0.48889], [0.00258, 0.48889], [0.0269, 0.48889], [0.05121, 0.48889], [0.07553, 0.48889], [0.09984, 0.48889], [0.12416, 0.48889], [0.14729, 0.48604], [0.15556, 0.46515], [0.15556, 0.44083], [0.15556, 0.41652], [0.15556, 0.3922], [0.15556, 0.36789], [0.15556, 0.34357], [0.15556, 0.31925], [0.15556, 0.29494], [0.15556, 0.27062], [0.15556, 0.24631], [0.15556, 0.22199], [0.15556, 0.19768], [0.15556, 0.17336], [0.15556, 0.14905], [0.15556, 0.12473], [0.15556, 0.10042], [0.15556, 0.0761], [0.15556, 0.05179], [0.15556, 0.02747], [0.15556, 0.00315], [0.15556, -0.02116], [0.15556, -0.04548], [0.15556, -0.06979], [0.15556, -0.09411], [0.15556, -0.11842], [0.16221, -0.13998], [0.1794, -0.15718], [0.19978, -0.16667], [0.2203, -0.17585], [0.24381, -0.17778], [0.26813, -0.17778], [0.29244, -0.17778], [0.31676, -0.17778], [0.34107, -0.17778], [0.36539, -0.17778], [0.38971, -0.17778], [0.41402, -0.17778], [0.43834, -0.17778], [0.46265, -0.17778], [0.48697, -0.17778], [0.48889, -0.20017], [0.48889, -0.22449], [0.48889, -0.2488], [0.48889, -0.27312], [0.48889, -0.29743], [0.48889, -0.32175], [0.48889, -0.34606], [0.48889, -0.37038], [0.48889, -0.39469], [0.48889, -0.41901], [0.48889, -0.44333], [0.48889, -0.46764], [0.48889, -0.49196], [0.47262, -0.5], [0.4483, -0.5], [0.42399, -0.5], [0.39967, -0.5], [0.37536, -0.5], [0.35104, -0.5], [0.32672, -0.5], [0.30241, -0.5], [0.27809, -0.5], [0.25378, -0.5], [0.22946, -0.5], [0.20515, -0.5], [0.18083, -0.5], [0.15652, -0.5], [0.1322, -0.5], [0.10789, -0.5], [0.08357, -0.5], [0.05925, -0.5], [0.03494, -0.5], [0.01062, -0.5], [-0.01369, -0.5], [-0.03801, -0.5], [-0.06232, -0.5], [-0.08664, -0.5], [-0.11095, -0.5], [-0.13527, -0.5], [-0.15958, -0.5], [-0.1839, -0.5], [-0.20821, -0.5], [-0.23253, -0.5], [-0.25685, -0.5], [-0.28116, -0.5], [-0.30548, -0.5], [-0.32979, -0.5], [-0.35411, -0.5], [-0.37842, -0.5], [-0.40274, -0.5], [-0.42705, -0.5], [-0.45137, -0.5], [-0.47568, -0.5]], "features": []}, {"name": "u", "outer": [[-0.48901, -0.49451], [-0.48901, -0.4622], [-0.5, -0.43444], [-0.5, -0.40214], [-0.5, -0.36983], [-0.5, -0.33752], [-0.5, -0.30522], [-0.5, -0.27291], [-0.5, -0.2406], [-0.5, -0.2083], [-0.5, -0.17599], [-0.5, -0.14368], [-0.5, -0.11138], [-0.5, -0.07907], [-0.5, -0.04676], [-0.5, -0.01446], [-0.5, 0.01785], [-0.5, 0.05016], [-0.5, 0.08246], [-0.5, 0.11477], [-0.5, 0.14708], [-0.5, 0.17938], [-0.5, 0.21169], [-0.5, 0.244], [-0.5, 0.2763], [-0.5, 0.30861], [-0.5, 0.34092], [-0.5, 0.37322], [-0.5, 0.40553], [-0.5, 0.43784], [-0.48901, 0.46559], [-0.47463, 0.48352], [-0.44232, 0.48352], [-0.41002, 0.48352], [-0.37771, 0.48352], [-0.3454, 0.48352], [-0.3131, 0.48352], [-0.28079, 0.48352], [-0.24848, 0.48352], [-0.21618, 0.48352], [-0.18387, 0.48352], [-0.15156, 0.48352], [-0.11926, 0.48352], [-0.08695, 0.48352], [-0.05464, 0.48352], [-0.02234, 0.48352], [0.00997, 0.48352], [0.04228, 0.48352], [0.07458, 0.48352], [0.10689, 0.48352], [0.1392, 0.48352], [0.1715, 0.48352], [0.20381, 0.48352], [0.23612, 0.48352], [0.26842, 0.48352], [0.30073, 0.48352], [0.33304, 0.48352], [0.36534, 0.48352], [0.39765, 0.48352], [0.42996, 0.48352], [0.46226, 0.48352], [0.48901, 0.47796], [0.47802, 0.4502], [0.47802, 0.4179], [0.47802, 0.38559], [0.47802, 0.35328], [0.47802, 0.32098], [0.47802, 0.28867], [0.47802, 0.25636], [0.47802, 0.22406], [0.47802, 0.19175], [0.47802, 0.15944], [0.47802, 0.12714], [0.47802, 0.09483], [0.47802, 0.06252], [0.47802, 0.03022], [0.47802, -0.00209], [0.47802, -0.0344], [0.47802, -0.0667], [0.47802, -0.09901], [0.47802, -0.13132], [0.47802, -0.16362], [0.47802, -0.19593], [0.47802, -0.22824], [0.47802, -0.26054], [0.47802, -0.29285], [0.47802, -0.32516], [0.47802, -0.35746], [0.47802, -0.38977], [0.47802, -0.42208], [0.47802, -0.45438], [0.47802, -0.48669], [0.45353, -0.49451], [0.42123, -0.49451], [0.38892, -0.49451], [0.35661, -0.49451], [0.32431, -0.49451], [0.292, -0.49451], [0.25969, -0.49451], [0.22739, -0.49451], [0.19508, -0.49451], [0.16499, -0.48916], [0.15934, -0.45919], [0.15934, -0.42689], [0.15934, -0.39458], [0.15934, -0.36227], [0.15934, -0.32997], [0.15934, -0.29766], [0.15934, -0.26535], [0.15934, -0.23305], [0.15934, -0.20074], [0.15934, -0.16843], [0.15934, -0.13613], [0.15934, -0.10382], [0.15934, -0.07151], [0.15934, -0.03921], [0.15934, -0.0069], [0.15934, 0.02541], [0.15934, 0.05771], [0.15934, 0.09002], [0.15832, 0.1219], [0.13547, 0.14475], [0.10694, 0.15385], [0.07918, 0.16484], [0.04687, 0.16484], [0.01457, 0.16484], [-0.01774, 0.16484], [-0.05005, 0.16484], [-0.08235, 0.16484], [-0.11165, 0.15758], [-0.14093, 0.15027], [-0.16378, 0.12743], [-0.18132, 0.10239], [-0.18132, 0.07008], [-0.18132, 0.03778], [-0.18132, 0.00547], [-0.18132, -0.02684], [-0.18132, -0.05914], [-0.18132, -0.09145], [-0.18132, -0.12376], [-0.18132, -0.15606], [-0.18132, -0.18837], [-0.18132, -0.22068], [-0.18132, -0.25298], [-0.18132, -0.28529], [-0.18132, -0.3176], [-0.18132, -0.3499], [-0.18132, -0.38221], [-0.18132, -0.41452], [-0.18132, -0.44682], [-0.18132, -0.47913], [-0.19825, -0.49451], [-0.23056, -0.49451], [-0.26286, -0.49451], [-0.29517, -0.49451], [-0.32748, -0.49451], [-0.35978, -0.49451], [-0.39209, -0.49451], [-0.4244, -0.49451], [-0.4567, -0.49451]], "features": []}, {"name": "mountain", "outer": [[-0.22727, -0.5], [-0.23864, -0.47857], [-0.24013, -0.45306], [-0.25, -0.43101], [-0.25298, -0.40611], [-0.26136, -0.38345], [-0.27387, -0.3625], [-0.28409, -0.3406], [-0.28672, -0.31555], [-0.29545, -0.29304], [-0.30682, -0.27161], [-0.30682, -0.24548], [-0.31818, -0.22405], [-0.32287, -0.19986], [-0.32955, -0.17649], [-0.33572, -0.15291], [-0.34091, -0.12893], [-0.34858, -0.10597], [-0.35227, -0.08137], [-0.36364, -0.05994], [-0.36384, -0.03389], [-0.375, -0.01238], [-0.37669, 0.01305], [-0.38636, 0.03518], [-0.39758, 0.05667], [-0.39773, 0.08274], [-0.40909, 0.10417], [-0.41284, 0.12875], [-0.42045, 0.15173], [-0.42569, 0.17569], [-0.43182, 0.19929], [-0.43855, 0.22264], [-0.44318, 0.24685], [-0.45455, 0.26828], [-0.45455, 0.29441], [-0.46591, 0.31584], [-0.4747, 0.33833], [-0.47727, 0.3634], [-0.48755, 0.38528], [-0.48864, 0.41096], [-0.48864, 0.4371], [-0.5, 0.45852], [-0.5, 0.48466], [-0.47785, 0.48864], [-0.45171, 0.48864], [-0.42558, 0.48864], [-0.39944, 0.48864], [-0.37331, 0.48864], [-0.34718, 0.48864], [-0.32104, 0.48864], [-0.29491, 0.48864], [-0.26877, 0.48864], [-0.24264, 0.48864], [-0.21651, 0.48864], [-0.19037, 0.48864], [-0.16424, 0.48864], [-0.1381, 0.48864], [-0.11197, 0.48864], [-0.08584, 0.48864], [-0.0597, 0.48864], [-0.03357, 0.48864], [-0.00743, 0.48864], [0.0187, 0.48864], [0.04483, 0.48864], [0.07097, 0.48864], [0.0971, 0.48864], [0.12323, 0.48864], [0.14937, 0.48864], [0.1755, 0.48864], [0.20164, 0.48864], [0.22777, 0.48864], [0.2539, 0.48864], [0.28004, 0.48864], [0.30617, 0.48864], [0.33231, 0.48864], [0.35844, 0.48864], [0.38457, 0.48864], [0.41071, 0.48864], [0.43684, 0.48864], [0.46298, 0.48864], [0.48864, 0.48816], [0.48864, 0.46203], [0.47727, 0.4406], [0.47727, 0.41447], [0.47727, 0.38833], [0.46591, 0.36691], [0.46581, 0.34081], [0.45455, 0.31935], [0.45296, 0.29387], [0.44318, 0.27179], [0.43207, 0.25025], [0.43182, 0.22423], [0.42045, 0.2028], [0.41681, 0.17817], [0.40909, 0.15524], [0.40396, 0.13123], [0.39773, 0.10768], [0.3911, 0.08429], [0.38636, 0.06012], [0.375, 0.03869], [0.375, 0.01255], [0.36364, -0.00887], [0.35495, -0.03141], [0.35227, -0.05643], [0.3421, -0.07835], [0.34091, -0.10399], [0.32955, -0.12542], [0.32684, -0.15043], [0.31818, -0.17298], [0.31399, -0.19738], [0.30682, -0.22054], [0.29545, -0.24197], [0.29545, -0.2681], [0.28409, -0.28953], [0.28409, -0.31566], [0.27273, -0.33709], [0.26498, -0.36002], [0.25, -0.37995], [0.25, -0.40608], [0.23864, -0.42751], [0.23124, -0.45058], [0.22727, -0.47507], [0.21839, -0.49752], [0.20794, -0.48067], [0.20455, -0.45595], [0.19318, -0.43452], [0.18182, -0.41309], [0.17045, -0.39166], [0.15909, -0.37024], [0.14528, -0.34982], [0.13484, -0.32802], [0.125, -0.30596], [0.11395, -0.2844], [0.1035, -0.26259], [0.09306, -0.24079], [0.08261, -0.21898], [0.07955, -0.19412], [0.06818, -0.17269], [0.05682, -0.15126], [0.04084, -0.13175], [0.0175, -0.125], [-0.00393, -0.11364], [-0.02536, -0.125], [-0.04972, -0.12927], [-0.06818, -0.14775], [-0.07865, -0.16955], [-0.08105, -0.19469], [-0.0915, -0.2165], [-0.10227, -0.23817], [-0.11364, -0.2596], [-0.125, -0.28102], [-0.13328, -0.30373], [-0.14372, -0.32554], [-0.15416, -0.34735], [-0.16461, -0.36915], [-0.18182, -0.38816], [-0.19318, -0.40959], [-0.20398, -0.43125], [-0.20638, -0.45638], [-0.21683, -0.47819]], "features": []}, {"name": "arrow", "outer": [[-0.0, -0.5], [0.01601, -0.48559], [0.03201, -0.47119], [0.04802, -0.45678], [0.06402, -0.44238], [0.08003, -0.42797], [0.09603, -0.41357], [0.11204, -0.39916], [0.12805, -0.38476], [0.14405, -0.37035], [0.16006, -0.35595], [0.17606, -0.34154], [0.19207, -0.32714], [0.20807, -0.31273], [0.22408, -0.29833], [0.24009, -0.28392], [0.25609, -0.26952], [0.2721, -0.25511], [0.2881, -0.24071], [0.30411, -0.2263], [0.32011, -0.2119], [0.33612, -0.19749], [0.35213, -0.18309], [0.36813, -0.16868], [0.38414, -0.15428], [0.40014, -0.13987], [0.41615, -0.12547], [0.43215, -0.11106], [0.44816, -0.09666], [0.46417, -0.08225], [0.48017, -0.06785], [0.49618, -0.05344], [0.48361, -0.05], [0.46208, -0.05], [0.44054, -0.05], [0.41901, -0.05], [0.39747, -0.05], [0.37594, -0.05], [0.35441, -0.05], [0.33287, -0.05], [0.31134, -0.05], [0.28981, -0.05], [0.26827, -0.05], [0.24674, -0.05], [0.22521, -0.05], [0.20367, -0.05], [0.18214, -0.05], [0.18, -0.03061], [0.18, -0.00907], [0.18, 0.01246], [0.18, 0.03399], [0.18, 0.05553], [0.18, 0.07706], [0.18, 0.0986], [0.18, 0.12013], [0.18, 0.14166], [0.18, 0.1632], [0.18, 0.18473], [0.18, 0.20626], [0.18, 0.2278], [0.18, 0.24933], [0.18, 0.27086], [0.18, 0.2924], [0.18, 0.31393], [0.18, 0.33546], [0.18, 0.357], [0.18, 0.37853], [0.18, 0.40006], [0.18, 0.4216], [0.18, 0.44313], [0.18, 0.46466], [0.18, 0.4862], [0.17227, 0.5], [0.15073, 0.5], [0.1292, 0.5], [0.10767, 0.5], [0.08613, 0.5], [0.0646, 0.5], [0.04307, 0.5], [0.02153, 0.5], [-0.0, 0.5], [-0.02153, 0.5], [-0.04307, 0.5], [-0.0646, 0.5], [-0.08613, 0.5], [-0.10767, 0.5], [-0.1292, 0.5], [-0.15073, 0.5], [-0.17227, 0.5], [-0.18, 0.4862], [-0.18, 0.46466], [-0.18, 0.44313], [-0.18, 0.4216], [-0.18, 0.40006], [-0.18, 0.37853], [-0.18, 0.357], [-0.18, 0.33546], [-0.18, 0.31393], [-0.18, 0.2924], [-0.18, 0.27086], [-0.18, 0.24933], [-0.18, 0.2278], [-0.18, 0.20626], [-0.18, 0.18473], [-0.18, 0.1632], [-0.18, 0.14166], [-0.18, 0.12013], [-0.18, 0.0986], [-0.18, 0.07706], [-0.18, 0.05553], [-0.18, 0.03399], [-0.18, 0.01246], [-0.18, -0.00907], [-0.18, -0.03061], [-0.18214, -0.05], [-0.20367, -0.05], [-0.22521, -0.05], [-0.24674, -0.05], [-0.26827, -0.05], [-0.28981, -0.05], [-0.31134, -0.05], [-0.33287, -0.05], [-0.35441, -0.05], [-0.37594, -0.05], [-0.39747, -0.05], [-0.41901, -0.05], [-0.44054, -0.05], [-0.46208, -0.05], [-0.48361, -0.05], [-0.49618, -0.05344], [-0.48017, -0.06785], [-0.46417, -0.08225], [-0.44816, -0.09666], [-0.43215, -0.11106], [-0.41615, -0.12547], [-0.40014, -0.13987], [-0.38414, -0.15428], [-0.36813, -0.16868], [-0.35213, -0.18309], [-0.33612, -0.19749], [-0.32011, -0.2119], [-0.30411, -0.2263], [-0.2881, -0.24071], [-0.2721, -0.25511], [-0.25609, -0.26952], [-0.24009, -0.28392], [-0.22408, -0.29833], [-0.20807, -0.31273], [-0.19207, -0.32714], [-0.17606, -0.34154], [-0.16006, -0.35595], [-0.14405, -0.37035], [-0.12805, -0.38476], [-0.11204, -0.39916], [-0.09603, -0.41357], [-0.08003, -0.42797], [-0.06402, -0.44238], [-0.04802, -0.45678], [-0.03201, -0.47119], [-0.01601, -0.48559]], "features": []}, {"name": "heart", "outer": [[-0.30899, -0.45506], [-0.32404, -0.44382], [-0.34344, -0.44308], [-0.35879, -0.43258], [-0.37459, -0.42316], [-0.38889, -0.41011], [-0.40575, -0.40324], [-0.41968, -0.38931], [-0.43361, -0.37538], [-0.44754, -0.36144], [-0.45506, -0.34485], [-0.46629, -0.3298], [-0.47345, -0.31307], [-0.47944, -0.29584], [-0.48876, -0.28], [-0.49141, -0.2614], [-0.5, -0.24525], [-0.5, -0.22555], [-0.5, -0.20584], [-0.5, -0.18614], [-0.5, -0.16644], [-0.5, -0.14673], [-0.5, -0.12703], [-0.5, -0.10733], [-0.48876, -0.09228], [-0.48844, -0.07271], [-0.47753, -0.05752], [-0.46852, -0.04155], [-0.46253, -0.02433], [-0.4486, -0.0104], [-0.43467, 0.00354], [-0.42073, 0.01747], [-0.4068, 0.0314], [-0.39888, 0.04782], [-0.38688, 0.06256], [-0.37295, 0.07649], [-0.35902, 0.09042], [-0.34509, 0.10435], [-0.33115, 0.11829], [-0.31722, 0.13222], [-0.30329, 0.14615], [-0.28936, 0.16008], [-0.27542, 0.17401], [-0.26149, 0.18795], [-0.24756, 0.20188], [-0.23363, 0.21581], [-0.21969, 0.22974], [-0.20576, 0.24368], [-0.19183, 0.25761], [-0.1779, 0.27154], [-0.16396, 0.28547], [-0.15003, 0.29941], [-0.1361, 0.31334], [-0.12217, 0.32727], [-0.10823, 0.3412], [-0.0943, 0.35514], [-0.08037, 0.36907], [-0.06644, 0.383], [-0.05251, 0.39693], [-0.03857, 0.41087], [-0.02464, 0.4248], [-0.01071, 0.43873], [0.00322, 0.43498], [0.01716, 0.42105], [0.03109, 0.40711], [0.04502, 0.39318], [0.05895, 0.37925], [0.07289, 0.36532], [0.08682, 0.35138], [0.10075, 0.33745], [0.11468, 0.32352], [0.12862, 0.30959], [0.14255, 0.29565], [0.15648, 0.28172], [0.17041, 0.26779], [0.18434, 0.25386], [0.19828, 0.23992], [0.21221, 0.22599], [0.22614, 0.21206], [0.24007, 0.19813], [0.25401, 0.1842], [0.26794, 0.17026], [0.28187, 0.15633], [0.2958, 0.1424], [0.30974, 0.12847], [0.32367, 0.11453], [0.3376, 0.1006], [0.35153, 0.08667], [0.36547, 0.07274], [0.3794, 0.0588], [0.39333, 0.04487], [0.40726, 0.03094], [0.4212, 0.01701], [0.43513, 0.00307], [0.44382, -0.01303], [0.45505, -0.02808], [0.46629, -0.04313], [0.47497, -0.05924], [0.47753, -0.07788], [0.48694, -0.09368], [0.48876, -0.11263], [0.48876, -0.13234], [0.48876, -0.15204], [0.48876, -0.17174], [0.48876, -0.19145], [0.48876, -0.21115], [0.48876, -0.23085], [0.48876, -0.25056], [0.48876, -0.27026], [0.47838, -0.28566], [0.4724, -0.30289], [0.46629, -0.32006], [0.45506, -0.33511], [0.44649, -0.35126], [0.43256, -0.3652], [0.41862, -0.37913], [0.40469, -0.39306], [0.39076, -0.40699], [0.37683, -0.42093], [0.3596, -0.42691], [0.34238, -0.4329], [0.3272, -0.44382], [0.31122, -0.45282], [0.29245, -0.45506], [0.27274, -0.45506], [0.25304, -0.45506], [0.23334, -0.45506], [0.21524, -0.45119], [0.19858, -0.44382], [0.18079, -0.43922], [0.16383, -0.43258], [0.14878, -0.42135], [0.13241, -0.41331], [0.11848, -0.39938], [0.10363, -0.38764], [0.08732, -0.37946], [0.07339, -0.36552], [0.0618, -0.35062], [0.05347, -0.33437], [0.03954, -0.32044], [0.01992, -0.32022], [0.00487, -0.30899], [-0.01213, -0.3155], [-0.02988, -0.32022], [-0.04658, -0.32748], [-0.06051, -0.34141], [-0.07444, -0.35534], [-0.08838, -0.36928], [-0.10231, -0.38321], [-0.11624, -0.39714], [-0.13057, -0.41011], [-0.1474, -0.41706], [-0.16133, -0.43099], [-0.17855, -0.43698], [-0.19542, -0.44382], [-0.21513, -0.44382], [-0.23022, -0.45494], [-0.24988, -0.45506], [-0.26958, -0.45506], [-0.28929, -0.45506]], "features": []}, {"name": "crescent", "outer": [[-0.47778, -0.32222], [-0.48399, -0.3049], [-0.48889, -0.28703], [-0.49641, -0.27025], [-0.5, -0.25185], [-0.5, -0.23195], [-0.5, -0.21206], [-0.5, -0.19216], [-0.5, -0.17226], [-0.5, -0.15237], [-0.5, -0.13247], [-0.5, -0.11258], [-0.5, -0.09268], [-0.5, -0.07279], [-0.49026, -0.05693], [-0.48889, -0.0376], [-0.47784, -0.02228], [-0.47778, -0.00241], [-0.46667, 0.01288], [-0.4592, 0.02969], [-0.45299, 0.04701], [-0.44444, 0.06336], [-0.43333, 0.07866], [-0.4265, 0.09572], [-0.41243, 0.10979], [-0.39837, 0.12386], [-0.38889, 0.13983], [-0.37809, 0.15525], [-0.36402, 0.16932], [-0.34995, 0.18338], [-0.33263, 0.1896], [-0.31856, 0.20366], [-0.30449, 0.21773], [-0.29042, 0.2318], [-0.2731, 0.23801], [-0.25587, 0.24444], [-0.24171, 0.25829], [-0.22528, 0.26667], [-0.20706, 0.27071], [-0.1901, 0.27778], [-0.1748, 0.28889], [-0.1551, 0.28935], [-0.13961, 0.3], [-0.11972, 0.3], [-0.09988, 0.30012], [-0.08453, 0.31111], [-0.06464, 0.31111], [-0.04474, 0.31111], [-0.02484, 0.31111], [-0.00495, 0.31111], [0.01495, 0.31111], [0.03484, 0.31111], [0.05474, 0.31111], [0.07003, 0.3], [0.08992, 0.3], [0.10982, 0.3], [0.12511, 0.28889], [0.14501, 0.28889], [0.16217, 0.28228], [0.17949, 0.27607], [0.19549, 0.26667], [0.21088, 0.25579], [0.2282, 0.24958], [0.24227, 0.23551], [0.25959, 0.2293], [0.27656, 0.22222], [0.29098, 0.20902], [0.30505, 0.19495], [0.31912, 0.18088], [0.33319, 0.16681], [0.34725, 0.15275], [0.36132, 0.13868], [0.37539, 0.12461], [0.38889, 0.1103], [0.39567, 0.09322], [0.40974, 0.07915], [0.41595, 0.06183], [0.43002, 0.04776], [0.43623, 0.03044], [0.44444, 0.01394], [0.44865, -0.00421], [0.45556, -0.02124], [0.46107, -0.03885], [0.46667, -0.05643], [0.46667, -0.07633], [0.47778, -0.09162], [0.48592, -0.10814], [0.48565, -0.12546], [0.47778, -0.1421], [0.47778, -0.162], [0.47778, -0.18189], [0.47778, -0.20179], [0.47778, -0.22168], [0.47195, -0.23916], [0.46667, -0.25687], [0.46667, -0.27677], [0.46117, -0.29438], [0.45556, -0.31195], [0.44875, -0.31541], [0.44254, -0.29809], [0.43333, -0.28201], [0.42226, -0.2667], [0.41605, -0.24938], [0.40198, -0.23531], [0.39577, -0.21799], [0.3817, -0.20392], [0.36763, -0.18985], [0.35356, -0.17578], [0.33949, -0.16172], [0.32543, -0.14765], [0.31136, -0.13358], [0.29729, -0.11951], [0.28087, -0.11111], [0.2659, -0.09923], [0.25029, -0.08889], [0.23451, -0.07895], [0.21719, -0.07274], [0.19986, -0.06653], [0.18451, -0.05556], [0.16847, -0.04625], [0.14933, -0.04444], [0.13383, -0.03383], [0.11414, -0.03333], [0.09884, -0.02222], [0.07895, -0.02222], [0.05905, -0.02222], [0.04376, -0.01111], [0.02387, -0.01111], [0.00397, -0.01111], [-0.01592, -0.01111], [-0.03582, -0.01111], [-0.05572, -0.01111], [-0.07561, -0.01111], [-0.0909, -0.02222], [-0.1108, -0.02222], [-0.13069, -0.02222], [-0.14599, -0.03333], [-0.16588, -0.03333], [-0.18118, -0.04444], [-0.20076, -0.0452], [-0.21636, -0.05556], [-0.23215, -0.06548], [-0.24947, -0.07169], [-0.26679, -0.0779], [-0.28086, -0.09197], [-0.29743, -0.1], [-0.31225, -0.11225], [-0.32632, -0.12632], [-0.34039, -0.14039], [-0.35446, -0.15446], [-0.36852, -0.16852], [-0.38259, -0.18259], [-0.39666, -0.19666], [-0.41073, -0.21073], [-0.4248, -0.2248], [-0.43333, -0.24116], [-0.44508, -0.25619], [-0.45556, -0.27174], [-0.46536, -0.28758], [-0.47157, -0.3049]], "features": []}, {"name": "cloud", "outer": [[-0.07778, -0.33889], [-0.09389, -0.32778], [-0.1146, -0.32778], [-0.13531, -0.32778], [-0.15263, -0.31959], [-0.17213, -0.31667], [-0.18843, -0.30601], [-0.20633, -0.29922], [-0.22423, -0.29244], [-0.24117, -0.28333], [-0.25678, -0.271], [-0.27339, -0.26111], [-0.28932, -0.24957], [-0.30561, -0.23889], [-0.32187, -0.22813], [-0.33651, -0.21349], [-0.35116, -0.19884], [-0.3658, -0.1842], [-0.37778, -0.16845], [-0.38724, -0.15165], [-0.40188, -0.13701], [-0.41111, -0.12012], [-0.42332, -0.10446], [-0.43333, -0.0879], [-0.44444, -0.07179], [-0.45154, -0.05402], [-0.45556, -0.03497], [-0.46512, -0.01822], [-0.46667, 0.00185], [-0.47778, 0.01796], [-0.47778, 0.03867], [-0.48889, 0.05478], [-0.48889, 0.0755], [-0.48889, 0.09621], [-0.49692, 0.11359], [-0.5, 0.13303], [-0.5, 0.15374], [-0.5, 0.17445], [-0.49949, 0.19495], [-0.48889, 0.21127], [-0.48889, 0.23199], [-0.48889, 0.2527], [-0.48889, 0.27341], [-0.48889, 0.29412], [-0.48889, 0.31483], [-0.48889, 0.32001], [-0.47661, 0.30439], [-0.46667, 0.28779], [-0.45517, 0.27184], [-0.44053, 0.2572], [-0.42588, 0.24255], [-0.41124, 0.2279], [-0.39334, 0.22112], [-0.37447, 0.21667], [-0.35754, 0.20754], [-0.33765, 0.20556], [-0.31694, 0.20556], [-0.29733, 0.20823], [-0.28012, 0.21667], [-0.26153, 0.2218], [-0.24363, 0.22859], [-0.22899, 0.24324], [-0.21434, 0.25788], [-0.19969, 0.27253], [-0.18505, 0.28717], [-0.17778, 0.30487], [-0.16886, 0.31667], [-0.16143, 0.30032], [-0.15464, 0.28242], [-0.13999, 0.26777], [-0.12535, 0.25313], [-0.1107, 0.23848], [-0.09443, 0.22778], [-0.07816, 0.21705], [-0.0576, 0.21667], [-0.04149, 0.20556], [-0.02078, 0.20556], [-7e-05, 0.20556], [0.02064, 0.20556], [0.039, 0.21123], [0.05746, 0.21667], [0.0748, 0.2248], [0.0927, 0.23159], [0.10735, 0.24624], [0.12199, 0.26088], [0.13664, 0.27553], [0.15128, 0.29017], [0.15556, 0.30912], [0.16667, 0.31462], [0.17491, 0.29732], [0.18169, 0.27942], [0.19634, 0.26477], [0.21099, 0.25013], [0.22563, 0.23548], [0.24315, 0.22778], [0.25926, 0.21667], [0.27933, 0.21511], [0.29608, 0.20556], [0.31679, 0.20556], [0.33751, 0.20556], [0.35822, 0.20556], [0.37534, 0.21423], [0.39504, 0.21667], [0.41115, 0.22778], [0.42904, 0.23459], [0.44368, 0.24924], [0.45833, 0.26388], [0.47297, 0.27853], [0.47976, 0.29643], [0.48889, 0.31336], [0.48889, 0.29926], [0.48889, 0.27855], [0.48889, 0.25784], [0.48889, 0.23713], [0.48889, 0.21641], [0.48889, 0.1957], [0.48889, 0.17499], [0.48889, 0.15428], [0.48889, 0.13357], [0.48889, 0.11286], [0.4794, 0.09607], [0.47778, 0.07603], [0.47778, 0.05532], [0.4669, 0.03912], [0.46667, 0.0185], [0.46117, 6e-05], [0.45556, -0.01832], [0.4476, -0.03574], [0.44081, -0.05364], [0.43333, -0.07125], [0.42222, -0.08736], [0.41259, -0.10408], [0.4058, -0.12198], [0.39115, -0.13663], [0.37778, -0.1518], [0.36972, -0.16917], [0.35507, -0.18382], [0.34043, -0.19846], [0.32578, -0.21311], [0.31114, -0.22775], [0.29504, -0.23889], [0.27859, -0.24919], [0.26395, -0.26383], [0.24671, -0.27222], [0.2306, -0.28333], [0.2135, -0.29206], [0.1956, -0.29884], [0.1777, -0.30563], [0.16156, -0.31667], [0.1419, -0.31921], [0.12474, -0.32778], [0.10403, -0.32778], [0.08331, -0.32778], [0.06705, -0.33851], [0.04649, -0.33889], [0.02578, -0.33889], [0.00507, -0.33889], [-0.01564, -0.33889], [-0.03635, -0.33889], [-0.05707, -0.33889]], "features": []}, {"name": "snowman", "outer": [[-0.08421, -0.5], [-0.09831, -0.48947], [-0.11648, -0.48878], [-0.13087, -0.47895], [-0.14567, -0.47012], [-0.16181, -0.46451], [-0.17486, -0.45145], [-0.191, -0.44584], [-0.20405, -0.43279], [-0.21711, -0.41974], [-0.23016, -0.40668], [-0.24211, -0.39317], [-0.24882, -0.37749], [-0.25443, -0.36136], [-0.26316, -0.34651], [-0.26316, -0.32805], [-0.26316, -0.30959], [-0.26316, -0.29113], [-0.26316, -0.27267], [-0.26316, -0.25421], [-0.26239, -0.23607], [-0.25263, -0.22165], [-0.25117, -0.2038], [-0.23811, -0.19075], [-0.23158, -0.17499], [-0.23158, -0.15653], [-0.23443, -0.13925], [-0.24748, -0.1262], [-0.26054, -0.11315], [-0.27359, -0.10009], [-0.28664, -0.08704], [-0.2997, -0.07399], [-0.30531, -0.05785], [-0.31836, -0.0448], [-0.33141, -0.03174], [-0.33702, -0.01561], [-0.34737, -0.00143], [-0.34824, 0.01666], [-0.35789, 0.03113], [-0.35946, 0.04894], [-0.36842, 0.06369], [-0.36842, 0.08215], [-0.36842, 0.10061], [-0.36842, 0.11907], [-0.36842, 0.13753], [-0.36842, 0.15599], [-0.36842, 0.17445], [-0.36227, 0.19036], [-0.35789, 0.20701], [-0.35789, 0.22547], [-0.34737, 0.23957], [-0.33983, 0.25491], [-0.33422, 0.27104], [-0.32117, 0.28409], [-0.31556, 0.30023], [-0.30251, 0.31328], [-0.28945, 0.32634], [-0.2764, 0.33939], [-0.26335, 0.35244], [-0.24721, 0.35805], [-0.23416, 0.37111], [-0.21802, 0.37672], [-0.20497, 0.38977], [-0.18883, 0.39538], [-0.17446, 0.40526], [-0.15964, 0.41404], [-0.1419, 0.41579], [-0.12737, 0.42526], [-0.10934, 0.42632], [-0.0951, 0.43648], [-0.07896, 0.44209], [-0.06591, 0.45515], [-0.05285, 0.4682], [-0.05263, 0.48657], [-0.03708, 0.48947], [-0.01862, 0.48947], [-0.00016, 0.48947], [0.0183, 0.48947], [0.03158, 0.48429], [0.03341, 0.46659], [0.04647, 0.45353], [0.05952, 0.44048], [0.07647, 0.43684], [0.09179, 0.42926], [0.10903, 0.42632], [0.12406, 0.41804], [0.14159, 0.41579], [0.15634, 0.40682], [0.17247, 0.40121], [0.18825, 0.39474], [0.20166, 0.38255], [0.21645, 0.37368], [0.23085, 0.36388], [0.24465, 0.35263], [0.26004, 0.34522], [0.27309, 0.33217], [0.28615, 0.31912], [0.29474, 0.30421], [0.30481, 0.28993], [0.31786, 0.27687], [0.32632, 0.26191], [0.33653, 0.24768], [0.34214, 0.23155], [0.34737, 0.21525], [0.34737, 0.19679], [0.35789, 0.18269], [0.35789, 0.16423], [0.35789, 0.14577], [0.35789, 0.12731], [0.35789, 0.10885], [0.35789, 0.09039], [0.35789, 0.07193], [0.34737, 0.05783], [0.34737, 0.03937], [0.34355, 0.0225], [0.33684, 0.00681], [0.32632, -0.00729], [0.31927, -0.02283], [0.31366, -0.03897], [0.30061, -0.05202], [0.29474, -0.06805], [0.28195, -0.08121], [0.26889, -0.09426], [0.25584, -0.10732], [0.24279, -0.12037], [0.22974, -0.13342], [0.22105, -0.14829], [0.22105, -0.16675], [0.22176, -0.18491], [0.23158, -0.19931], [0.24042, -0.2141], [0.24603, -0.23024], [0.25263, -0.24597], [0.25263, -0.26443], [0.25263, -0.28289], [0.25263, -0.30135], [0.25263, -0.31981], [0.25263, -0.33827], [0.24229, -0.35244], [0.24211, -0.37083], [0.23107, -0.38472], [0.22105, -0.39903], [0.21241, -0.41391], [0.19936, -0.42696], [0.1863, -0.44001], [0.17089, -0.44737], [0.15711, -0.45868], [0.14269, -0.46842], [0.12792, -0.47734], [0.11013, -0.47895], [0.09565, -0.48856], [0.07757, -0.48947], [0.06338, -0.49978], [0.04501, -0.5], [0.02655, -0.5], [0.00809, -0.5], [-0.01037, -0.5], [-0.02883, -0.5], [-0.04729, -0.5], [-0.06575, -0.5]], "features": []}, {"name": "l", "outer": [[-0.42, -0.5], [-0.397, -0.5], [-0.374, -0.5], [-0.351, -0.5], [-0.328, -0.5], [-0.305, -0.5], [-0.282, -0.5], [-0.259, -0.5], [-0.236, -0.5], [-0.213, -0.5], [-0.19, -0.5], [-0.167, -0.5], [-0.144, -0.5], [-0.121, -0.5], [-0.098, -0.5], [-0.075, -0.5], [-0.06, -0.492], [-0.06, -0.469], [-0.06, -0.446], [-0.06, -0.423], [-0.06, -0.4], [-0.06, -0.377], [-0.06, -0.354], [-0.06, -0.331], [-0.06, -0.308], [-0.06, -0.285], [-0.06, -0.262], [-0.06, -0.239], [-0.06, -0.216], [-0.06, -0.193], [-0.06, -0.17], [-0.06, -0.147], [-0.06, -0.124], [-0.06, -0.101], [-0.06, -0.078], [-0.06, -0.055], [-0.06, -0.032], [-0.06, -0.009], [-0.06, 0.014], [-0.06, 0.037], [-0.06, 0.06], [-0.06, 0.083], [-0.06, 0.106], [-0.06, 0.129], [-0.048, 0.14], [-0.025, 0.14], [-0.002, 0.14], [0.021, 0.14], [0.044, 0.14], [0.067, 0.14], [0.09, 0.14], [0.113, 0.14], [0.136, 0.14], [0.159, 0.14], [0.182, 0.14], [0.205, 0.14], [0.228, 0.14], [0.251, 0.14], [0.274, 0.14], [0.297, 0.14], [0.32, 0.14], [0.343, 0.14], [0.366, 0.14], [0.389, 0.14], [0.412, 0.14], [0.42, 0.155], [0.42, 0.178], [0.42, 0.201], [0.42, 0.224], [0.42, 0.247], [0.42, 0.27], [0.42, 0.293], [0.42, 0.316], [0.42, 0.339], [0.42, 0.362], [0.42, 0.385], [0.42, 0.408], [0.42, 0.431], [0.42, 0.454], [0.42, 0.477], [0.42, 0.5], [0.397, 0.5], [0.374, 0.5], [0.351, 0.5], [0.328, 0.5], [0.305, 0.5], [0.282, 0.5], [0.259, 0.5], [0.236, 0.5], [0.213, 0.5], [0.19, 0.5], [0.167, 0.5], [0.144, 0.5], [0.121, 0.5], [0.098, 0.5], [0.075, 0.5], [0.052, 0.5], [0.029, 0.5], [0.006, 0.5], [-0.017, 0.5], [-0.04, 0.5], [-0.063, 0.5], [-0.086, 0.5], [-0.109, 0.5], [-0.132, 0.5], [-0.155, 0.5], [-0.178, 0.5], [-0.201, 0.5], [-0.224, 0.5], [-0.247, 0.5], [-0.27, 0.5], [-0.293, 0.5], [-0.316, 0.5], [-0.339, 0.5], [-0.362, 0.5], [-0.385, 0.5], [-0.408, 0.5], [-0.42, 0.489], [-0.42, 0.466], [-0.42, 0.443], [-0.42, 0.42], [-0.42, 0.397], [-0.42, 0.374], [-0.42, 0.351], [-0.42, 0.328], [-0.42, 0.305], [-0.42, 0.282], [-0.42, 0.259], [-0.42, 0.236], [-0.42, 0.213], [-0.42, 0.19], [-0.42, 0.167], [-0.42, 0.144], [-0.42, 0.121], [-0.42, 0.098], [-0.42, 0.075], [-0.42, 0.052], [-0.42, 0.029], [-0.42, 0.006], [-0.42, -0.017], [-0.42, -0.04], [-0.42, -0.063], [-0.42, -0.086], [-0.42, -0.109], [-0.42, -0.132], [-0.42, -0.155], [-0.42, -0.178], [-0.42, -0.201], [-0.42, -0.224], [-0.42, -0.247], [-0.42, -0.27], [-0.42, -0.293], [-0.42, -0.316], [-0.42, -0.339], [-0.42, -0.362], [-0.42, -0.385], [-0.42, -0.408], [-0.42, -0.431], [-0.42, -0.454], [-0.42, -0.477]], "features": []}, {"name": "trapezoid", "outer": [[-0.26987, -0.5], [-0.24742, -0.5], [-0.22497, -0.5], [-0.20252, -0.5], [-0.18006, -0.5], [-0.15761, -0.5], [-0.13516, -0.5], [-0.11271, -0.5], [-0.09026, -0.5], [-0.06781, -0.5], [-0.04536, -0.5], [-0.0229, -0.5], [-0.00045, -0.5], [0.022, -0.5], [0.04445, -0.5], [0.0669, -0.5], [0.08935, -0.5], [0.1118, -0.5], [0.13426, -0.5], [0.15671, -0.5], [0.17916, -0.5], [0.20161, -0.5], [0.22406, -0.5], [0.24651, -0.5], [0.26896, -0.5], [0.2749, -0.47926], [0.27993, -0.45738], [0.28497, -0.4355], [0.29, -0.41362], [0.29503, -0.39174], [0.30006, -0.36986], [0.3051, -0.34798], [0.31013, -0.3261], [0.31516, -0.30422], [0.32019, -0.28234], [0.32523, -0.26046], [0.33026, -0.23858], [0.33529, -0.2167], [0.34032, -0.19482], [0.34536, -0.17294], [0.35039, -0.15106], [0.35542, -0.12918], [0.36045, -0.1073], [0.36549, -0.08542], [0.37052, -0.06354], [0.37555, -0.04166], [0.38058, -0.01978], [0.38561, 0.0021], [0.39065, 0.02398], [0.39568, 0.04586], [0.40071, 0.06774], [0.40574, 0.08962], [0.41078, 0.11151], [0.41581, 0.13339], [0.42084, 0.15527], [0.42587, 0.17715], [0.43091, 0.19903], [0.43594, 0.22091], [0.44097, 0.24279], [0.446, 0.26467], [0.45104, 0.28655], [0.45607, 0.30843], [0.4611, 0.33031], [0.46613, 0.35219], [0.47117, 0.37407], [0.4762, 0.39595], [0.48123, 0.41783], [0.48626, 0.43971], [0.4913, 0.46159], [0.49633, 0.48347], [0.49464, 0.5], [0.47219, 0.5], [0.44974, 0.5], [0.42729, 0.5], [0.40484, 0.5], [0.38239, 0.5], [0.35994, 0.5], [0.33748, 0.5], [0.31503, 0.5], [0.29258, 0.5], [0.27013, 0.5], [0.24768, 0.5], [0.22523, 0.5], [0.20278, 0.5], [0.18033, 0.5], [0.15787, 0.5], [0.13542, 0.5], [0.11297, 0.5], [0.09052, 0.5], [0.06807, 0.5], [0.04562, 0.5], [0.02317, 0.5], [0.00071, 0.5], [-0.02174, 0.5], [-0.04419, 0.5], [-0.06664, 0.5], [-0.08909, 0.5], [-0.11154, 0.5], [-0.13399, 0.5], [-0.15645, 0.5], [-0.1789, 0.5], [-0.20135, 0.5], [-0.2238, 0.5], [-0.24625, 0.5], [-0.2687, 0.5], [-0.29115, 0.5], [-0.3136, 0.5], [-0.33606, 0.5], [-0.35851, 0.5], [-0.38096, 0.5], [-0.40341, 0.5], [-0.42586, 0.5], [-0.44831, 0.5], [-0.47076, 0.5], [-0.49322, 0.5], [-0.49633, 0.4846], [-0.4913, 0.46272], [-0.48626, 0.44084], [-0.48123, 0.41896], [-0.4762, 0.39708], [-0.47117, 0.3752], [-0.46613, 0.35332], [-0.4611, 0.33144], [-0.45607, 0.30956], [-0.45104, 0.28768], [-0.446, 0.2658], [-0.44097, 0.24392], [-0.43594, 0.22204], [-0.43091, 0.20016], [-0.42587, 0.17828], [-0.42084, 0.1564], [-0.41581, 0.13452], [-0.41078, 0.11264], [-0.40574, 0.09076], [-0.40071, 0.06888], [-0.39568, 0.047], [-0.39065, 0.02512], [-0.38561, 0.00324], [-0.38058, -0.01864], [-0.37555, -0.04052], [-0.37052, -0.0624], [-0.36549, -0.08428], [-0.36045, -0.10616], [-0.35542, -0.12804], [-0.35039, -0.14992], [-0.34536, -0.1718], [-0.34032, -0.19368], [-0.33529, -0.21556], [-0.33026, -0.23744], [-0.32523, -0.25932], [-0.32019, -0.2812], [-0.31516, -0.30308], [-0.31013, -0.32496], [-0.3051, -0.34684], [-0.30006, -0.36872], [-0.29503, -0.3906], [-0.29, -0.41248], [-0.28497, -0.43436], [-0.27993, -0.45624], [-0.2749, -0.47812]], "features": []}, {"name": "pacman", "outer": [[0.0, -0.06038], [0.01384, -0.07688], [0.02768, -0.09337], [0.04152, -0.10986], [0.05535, -0.12635], [0.06919, -0.14284], [0.08303, -0.15934], [0.09687, -0.17583], [0.11071, -0.19232], [0.12455, -0.20881], [0.13838, -0.2253], [0.15222, -0.2418], [0.16606, -0.25829], [0.1799, -0.27478], [0.19374, -0.29127], [0.20758, -0.30776], [0.22141, -0.32425], [0.23525, -0.34075], [0.24909, -0.35724], [0.26293, -0.37373], [0.27677, -0.39022], [0.29061, -0.40671], [0.30444, -0.42321], [0.31828, -0.4397], [0.33393, -0.43269], [0.34965, -0.41798], [0.36473, -0.40262], [0.37913, -0.38662], [0.39283, -0.37001], [0.40578, -0.35281], [0.41797, -0.33507], [0.42939, -0.31682], [0.44002, -0.2981], [0.44983, -0.27894], [0.45881, -0.25938], [0.46694, -0.23945], [0.4742, -0.21919], [0.48059, -0.19863], [0.4861, -0.17783], [0.4907, -0.1568], [0.4944, -0.1356], [0.49718, -0.11425], [0.49904, -0.09281], [0.49998, -0.0713], [0.5, -0.04978], [0.49909, -0.02827], [0.49726, -0.00682], [0.49451, 0.01453], [0.49085, 0.03574], [0.48627, 0.05677], [0.48076, 0.07759], [0.47436, 0.09814], [0.46709, 0.1184], [0.45895, 0.13833], [0.44996, 0.15789], [0.44015, 0.17704], [0.42951, 0.19576], [0.41809, 0.214], [0.40589, 0.23174], [0.39295, 0.24893], [0.37927, 0.26555], [0.3649, 0.28157], [0.34985, 0.29696], [0.33415, 0.31169], [0.31784, 0.32573], [0.30093, 0.33906], [0.28347, 0.35165], [0.26549, 0.36348], [0.24701, 0.37452], [0.22808, 0.38476], [0.20871, 0.39417], [0.18895, 0.40271], [0.16884, 0.4104], [0.14842, 0.41721], [0.12773, 0.42315], [0.1068, 0.42818], [0.08568, 0.43232], [0.0644, 0.43554], [0.043, 0.43785], [0.02152, 0.43924], [0.0, 0.4397], [-0.02152, 0.43924], [-0.043, 0.43785], [-0.0644, 0.43554], [-0.08568, 0.43232], [-0.1068, 0.42818], [-0.12773, 0.42315], [-0.14842, 0.41721], [-0.16884, 0.4104], [-0.18895, 0.40271], [-0.20871, 0.39417], [-0.22808, 0.38476], [-0.24701, 0.37452], [-0.26549, 0.36348], [-0.28347, 0.35165], [-0.30093, 0.33906], [-0.31784, 0.32573], [-0.33415, 0.31169], [-0.34985, 0.29696], [-0.3649, 0.28157], [-0.37927, 0.26555], [-0.39295, 0.24893], [-0.40589, 0.23174], [-0.41809, 0.214], [-0.42951, 0.19576], [-0.44015, 0.17704], [-0.44996, 0.15789], [-0.45895, 0.13833], [-0.46709, 0.1184], [-0.47436, 0.09814], [-0.48076, 0.07759], [-0.48627, 0.05677], [-0.49085, 0.03574], [-0.49451, 0.01453], [-0.49726, -0.00682], [-0.49909, -0.02827], [-0.5, -0.04978], [-0.49998, -0.0713], [-0.49904, -0.09281], [-0.49718, -0.11425], [-0.4944, -0.1356], [-0.4907, -0.1568], [-0.4861, -0.17783], [-0.48059, -0.19863], [-0.4742, -0.21919], [-0.46694, -0.23945], [-0.45881, -0.25938], [-0.44983, -0.27894], [-0.44002, -0.2981], [-0.42939, -0.31682], [-0.41797, -0.33507], [-0.40578, -0.35281], [-0.39283, -0.37001], [-0.37913, -0.38662], [-0.36473, -0.40262], [-0.34965, -0.41798], [-0.33393, -0.43269], [-0.31828, -0.4397], [-0.30444, -0.42321], [-0.29061, -0.40671], [-0.27677, -0.39022], [-0.26293, -0.37373], [-0.24909, -0.35724], [-0.23525, -0.34075], [-0.22141, -0.32425], [-0.20758, -0.30776], [-0.19374, -0.29127], [-0.1799, -0.27478], [-0.16606, -0.25829], [-0.15222, -0.2418], [-0.13838, -0.2253], [-0.12455, -0.20881], [-0.11071, -0.19232], [-0.09687, -0.17583], [-0.08303, -0.15934], [-0.06919, -0.14284], [-0.05535, -0.12635], [-0.04152, -0.10986], [-0.02768, -0.09337], [-0.01384, -0.07688]], "features": []}]
package/src/detect.js ADDED
@@ -0,0 +1,417 @@
1
+ // Port of glyphbyte/detect.py: frames, baseline, start dot, rectification. Same numbers, same order.
2
+ import { resizeGray, adaptiveThreshold, morphClose3, removeSpecks, labelComponents, distanceTransform, dilate, warp, normalizePatch } from './imgops.js';
3
+ import { convexHull, polygonArea, polygonPerimeter, polygonCentroid, pointInPolygon, approxPolyDP, maxInscribedQuad, eigen2, homography, mat3mul, mat3inv } from './geom.js';
4
+
5
+ export const PATCH = 64, PATCH_MARGIN = 0.12, MAX_SIDE = 1280, JUNK_TOP_K = 24;
6
+ export const FRAME_SQUARE = 0, FRAME_CIRCLE = 1;
7
+
8
+ export function prepare(gray, W, H) {
9
+ const m = Math.max(W, H);
10
+ if (m <= MAX_SIDE) return { gray, W, H, scale: 1 };
11
+ const s = MAX_SIDE / m, nw = Math.round(W * s), nh = Math.round(H * s);
12
+ return { gray: resizeGray(gray, W, H, nw, nh), W: nw, H: nh, scale: s };
13
+ }
14
+
15
+ export function binarize(gray, W, H, light) {
16
+ const side = Math.min(W, H), block = Math.max(31, Math.floor(side / 14) | 1);
17
+ const ink = morphClose3(adaptiveThreshold(gray, W, H, block, 10, light), W, H);
18
+ return removeSpecks(ink, W, H, Math.max(6, (side / 250) ** 2));
19
+ }
20
+
21
+ // rasterize a convex polygon into a w x h mask whose origin is (x0, y0)
22
+ function fillConvex(poly, x0, y0, w, h) {
23
+ const mask = new Uint8Array(w * h), n = poly.length;
24
+ for (let y = 0; y < h; y++) {
25
+ const yy = y + y0 + 0.5; let xl = Infinity, xr = -Infinity;
26
+ for (let i = 0; i < n; i++) {
27
+ const [ax, ay] = poly[i], [bx, by] = poly[(i + 1) % n];
28
+ if ((ay <= yy && by > yy) || (by <= yy && ay > yy)) { const x = ax + (yy - ay) * (bx - ax) / (by - ay); if (x < xl) xl = x; if (x > xr) xr = x; }
29
+ }
30
+ if (xl === Infinity) continue;
31
+ const xa = Math.max(0, Math.ceil(xl - x0 - 0.5)), xb = Math.min(w - 1, Math.floor(xr - x0 - 0.5));
32
+ for (let x = xa; x <= xb; x++) mask[y * w + x] = 1;
33
+ }
34
+ return mask;
35
+ }
36
+
37
+ function rayStroke(ink, region, w, h, cx, cy, size, nRays = 16) {
38
+ const limit = Math.max(4, Math.floor(0.35 * size)), slackMax = Math.max(2, Math.floor(0.08 * size)), runs = [];
39
+ for (let k = 0; k < nRays; k++) {
40
+ const a = 2 * Math.PI * k / nRays, dx = Math.cos(a), dy = Math.sin(a);
41
+ let x = cx, y = cy, inside = true, run = 0, slack = 0;
42
+ for (let s = 0; s < Math.floor(0.9 * size) + limit + slackMax; s++) {
43
+ x += dx; y += dy; const xi = Math.round(x), yi = Math.round(y);
44
+ if (xi < 0 || yi < 0 || xi >= w || yi >= h) break;
45
+ if (inside) { if (region[yi * w + xi]) continue; inside = false; }
46
+ if (ink[yi * w + xi]) { run++; if (run > limit) break; }
47
+ else if (run > 0) break;
48
+ else { slack++; if (slack > slackMax) break; }
49
+ }
50
+ runs.push(run);
51
+ }
52
+ const pos = runs.filter(r => r > 0).sort((a, b) => a - b);
53
+ if (pos.length < nRays / 2) return 0;
54
+ return pos.length % 2 ? pos[(pos.length - 1) / 2] : (pos[pos.length / 2 - 1] + pos[pos.length / 2]) / 2;
55
+ }
56
+
57
+ function classifyKind(hull) {
58
+ const { quad, area: qa } = maxInscribedQuad(hull), ha = polygonArea(hull), ratio = qa / Math.max(ha, 1e-6);
59
+ const nv = approxPolyDP(hull, 0.02 * polygonPerimeter(hull)).length;
60
+ const score = (ratio - 0.80) / 0.06 + (5.5 - nv) / 3.0;
61
+ return { kind: score > 0 ? FRAME_SQUARE : FRAME_CIRCLE, conf: Math.min(1, Math.abs(score) / 1.5), quad };
62
+ }
63
+
64
+ // boundary pixels of component k within its bbox
65
+ function boundaryPoints(labels, W, H, k, s) {
66
+ const pts = [];
67
+ for (let y = s.y0; y <= s.y1; y++) for (let x = s.x0; x <= s.x1; x++) {
68
+ const i = y * W + x; if (labels[i] !== k) continue;
69
+ if (x === 0 || y === 0 || x === W - 1 || y === H - 1 || labels[i - 1] !== k || labels[i + 1] !== k || labels[i - W] !== k || labels[i + W] !== k) pts.push([x, y]);
70
+ }
71
+ return pts;
72
+ }
73
+
74
+ export function findFrames(ink, W, H) {
75
+ const side = Math.min(W, H), minSize = side / 45, maxSize = side / 1.15, out = [];
76
+ const bg = new Uint8Array(W * H); for (let i = 0; i < bg.length; i++) bg[i] = ink[i] ? 0 : 1;
77
+ const bgLab = labelComponents(bg, W, H, 4), inkLab = labelComponents(ink, W, H, 8);
78
+
79
+ const consider = (hull, regionArea, ringOuter) => {
80
+ if (hull.length < 3) return;
81
+ let bx0 = Infinity, by0 = Infinity, bx1 = -Infinity, by1 = -Infinity;
82
+ for (const [x, y] of hull) { if (x < bx0) bx0 = x; if (y < by0) by0 = y; if (x > bx1) bx1 = x; if (y > by1) by1 = y; }
83
+ const bw = bx1 - bx0 + 1, bh = by1 - by0 + 1, big = Math.max(bw, bh);
84
+ if (big < minSize || big > maxSize) return;
85
+ if (big / Math.max(1, Math.min(bw, bh)) > 2.2) return;
86
+ const hullArea = polygonArea(hull);
87
+ if (hullArea <= 0 || regionArea < 20 || regionArea / hullArea < 0.35) return;
88
+ const pad = Math.floor(0.5 * big) + 4, x0 = Math.max(0, Math.floor(bx0) - pad), y0 = Math.max(0, Math.floor(by0) - pad);
89
+ const x1 = Math.min(W, Math.ceil(bx1) + pad), y1 = Math.min(H, Math.ceil(by1) + pad), w = x1 - x0, h = y1 - y0;
90
+ if (w <= 0 || h <= 0) return;
91
+ const mask = fillConvex(hull, x0, y0, w, h);
92
+ let px = 0; for (let i = 0; i < mask.length; i++) px += mask[i];
93
+ if (px < 20) return;
94
+ const inkRoi = new Uint8Array(w * h), inside = new Uint8Array(w * h);
95
+ let inkIn = 0, sx = 0, sy = 0;
96
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { const v = ink[(y + y0) * W + x + x0]; inkRoi[y * w + x] = v; if (v && mask[y * w + x]) { inside[y * w + x] = 1; inkIn++; sx += x; sy += y; } }
97
+ const frac = inkIn / px;
98
+ if (frac < 0.015 || frac > 0.75) return;
99
+ let size = Math.sqrt(px);
100
+ const c = polygonCentroid(hull), center = [c[0], c[1]];
101
+ const stroke = rayStroke(inkRoi, mask, w, h, center[0] - x0, center[1] - y0, size);
102
+ if (stroke < 1 || stroke > 0.16 * size) return;
103
+ const { kind, conf, quad } = classifyKind(hull);
104
+ const offC = inkIn ? Math.hypot(sx / inkIn + x0 - center[0], sy / inkIn + y0 - center[1]) / size : 1;
105
+ const cc = labelComponents(inside, w, h, 8);
106
+ let share = 0; if (cc.n) { let m = 0; for (let k = 1; k <= cc.n; k++) m = Math.max(m, cc.stats[k].area); share = m / Math.max(1, inkIn); }
107
+ let q = 1;
108
+ q *= (frac >= 0.04 && frac <= 0.5) ? 1 : 0.5;
109
+ q *= (stroke / size >= 0.02 && stroke / size <= 0.10) ? 1 : 0.5;
110
+ q *= 0.5 + 0.5 * conf;
111
+ q *= offC <= 0.22 ? 1 : 0.5;
112
+ q *= share >= 0.5 ? 1 : (share >= 0.3 ? 0.6 : 0.3);
113
+ if (kind === FRAME_CIRCLE) size = Math.sqrt(px / Math.PI) * 2;
114
+ const fr = { kind, center, size, hull, outer: ringOuter || hull, corners: null, ellipse: null, inkFraction: frac, conf, stroke, quality: q, lightInk: false, junk: 0 };
115
+ if (kind === FRAME_SQUARE && quad) fr.corners = quad.map(p => [center[0] + (p[0] - center[0]) * (1 + 0.5 * stroke / Math.max(size, 1)), center[1] + (p[1] - center[1]) * (1 + 0.5 * stroke / Math.max(size, 1))]);
116
+ // ellipse from the second moments of the hull mask
117
+ let m00 = 0, mx = 0, my = 0;
118
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (mask[y * w + x]) { m00++; mx += x; my += y; }
119
+ mx /= m00; my /= m00;
120
+ let cxx = 0, cxy = 0, cyy = 0;
121
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (mask[y * w + x]) { const dx = x - mx, dy = y - my; cxx += dx * dx; cxy += dx * dy; cyy += dy * dy; }
122
+ cxx /= m00; cxy /= m00; cyy /= m00;
123
+ const e = eigen2(cxx, cxy, cyy);
124
+ fr.ellipse = { cx: mx + x0, cy: my + y0, MA: 4 * Math.sqrt(Math.max(e.evals[0], 1e-6)) + stroke, ma: 4 * Math.sqrt(Math.max(e.evals[1], 1e-6)) + stroke, ang: Math.atan2(e.evecs[0][1], e.evecs[0][0]) * 180 / Math.PI };
125
+ out.push(fr);
126
+ };
127
+
128
+ // A) holes: background components not touching the border
129
+ for (let k = 1; k <= bgLab.n; k++) {
130
+ const s = bgLab.stats[k]; if (s.border) continue;
131
+ const big = Math.max(s.x1 - s.x0 + 1, s.y1 - s.y0 + 1); if (big < minSize || big > maxSize) continue;
132
+ consider(convexHull(boundaryPoints(bgLab.labels, W, H, k, s)), s.area, null);
133
+ }
134
+ // B) rings with a pen gap: low-solidity ink components whose hull encloses other ink
135
+ for (let k = 1; k <= inkLab.n; k++) {
136
+ const s = inkLab.stats[k]; const big = Math.max(s.x1 - s.x0 + 1, s.y1 - s.y0 + 1);
137
+ if (big < minSize || big > maxSize) continue;
138
+ const hull = convexHull(boundaryPoints(inkLab.labels, W, H, k, s)); if (hull.length < 3) continue;
139
+ const ha = polygonArea(hull); if (ha <= 0 || s.area / ha > 0.5) continue;
140
+ const c = polygonCentroid(hull), shrink = 1 - 0.06;
141
+ const inner = hull.map(p => [c[0] + (p[0] - c[0]) * shrink, c[1] + (p[1] - c[1]) * shrink]);
142
+ consider(inner, polygonArea(inner), hull);
143
+ }
144
+ // dedupe
145
+ out.sort((a, b) => b.size - a.size);
146
+ const keep = [];
147
+ for (const f of out) {
148
+ let dup = false;
149
+ for (const g of keep) {
150
+ if (Math.hypot(f.center[0] - g.center[0], f.center[1] - g.center[1]) < 0.35 * g.size && f.size < 1.3 * g.size) { dup = true; break; }
151
+ if (f.size > 0.3 * g.size && f.size < 0.8 * g.size && g.quality >= 0.7 * f.quality && pointInPolygon(g.hull, f.center[0], f.center[1])) { dup = true; break; }
152
+ }
153
+ if (!dup) keep.push(f);
154
+ }
155
+ return keep;
156
+ }
157
+
158
+ export function filterRow(frames) {
159
+ const warnings = [], n = frames.length;
160
+ if (n <= 1) return { frames, warnings };
161
+ const pts = frames.map(f => f.center), sizes = frames.map(f => f.size), quality = frames.map(f => f.quality);
162
+ let best = [], bestQ = -1;
163
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
164
+ const sRef = (sizes[i] + sizes[j]) / 2, r = sizes[i] / sizes[j];
165
+ if (r < 0.6 || r > 1.67) continue;
166
+ let dx = pts[j][0] - pts[i][0], dy = pts[j][1] - pts[i][1]; const L = Math.hypot(dx, dy);
167
+ if (L < 0.8 * sRef) continue;
168
+ dx /= L; dy /= L; const nx = -dy, ny = dx;
169
+ let ok = [];
170
+ for (let k = 0; k < n; k++) { const off = Math.abs((pts[k][0] - pts[i][0]) * nx + (pts[k][1] - pts[i][1]) * ny); const sr = sizes[k] / sRef; if (off <= 0.45 * sRef && sr >= 0.4 && sr <= 2.5) ok.push(k); }
171
+ const t = k => (pts[k][0] - pts[i][0]) * dx + (pts[k][1] - pts[i][1]) * dy;
172
+ ok.sort((a, b) => t(a) - t(b));
173
+ if (ok.length >= 3) { // smooth size trend
174
+ const ts = ok.map(t), sz = ok.map(k => sizes[k]), mT = ts.reduce((a, b) => a + b, 0) / ts.length, mS = sz.reduce((a, b) => a + b, 0) / sz.length;
175
+ let num = 0, den = 0; for (let k = 0; k < ts.length; k++) { num += (ts[k] - mT) * (sz[k] - mS); den += (ts[k] - mT) ** 2; }
176
+ const slope = den > 1e-9 ? num / den : 0, icpt = mS - slope * mT;
177
+ ok = ok.filter((k, idx) => { const tr = slope * ts[idx] + icpt, a = sz[idx] / Math.max(tr, 1e-6); return a >= 0.65 && a <= 1.5; });
178
+ }
179
+ const spaced = [];
180
+ for (const k of ok) if (!spaced.length || t(k) - t(spaced[spaced.length - 1]) >= 0.8 * Math.min(sizes[k], sizes[spaced[spaced.length - 1]])) spaced.push(k);
181
+ const q = spaced.reduce((a, k) => a + quality[k], 0);
182
+ if (q > bestQ + 1e-9) { bestQ = q; best = spaced; }
183
+ }
184
+ if (best.length < 2) { let bi = 0; for (let k = 1; k < n; k++) if (quality[k] * (1 + 0.001 * sizes[k]) > quality[bi] * (1 + 0.001 * sizes[bi])) bi = k; best = [bi]; }
185
+ if (best.length < n) warnings.push(`dropped ${n - best.length} frame(s) that were not on the row`);
186
+ return { frames: best.map(k => frames[k]), warnings };
187
+ }
188
+
189
+ function rowAxis(frames) {
190
+ if (frames.length < 2) return [1, 0];
191
+ const mx = frames.reduce((s, f) => s + f.center[0], 0) / frames.length, my = frames.reduce((s, f) => s + f.center[1], 0) / frames.length;
192
+ let a = 0, b = 0, d = 0; for (const f of frames) { const dx = f.center[0] - mx, dy = f.center[1] - my; a += dx * dx; b += dx * dy; d += dy * dy; }
193
+ return eigen2(a, b, d).evecs[0];
194
+ }
195
+ function sampleLine(inkD, W, H, p0, d, t0, t1) {
196
+ const ts = [], hit = [];
197
+ for (let t = t0; t < t1; t += 1) { const x = Math.round(p0[0] + t * d[0]), y = Math.round(p0[1] + t * d[1]); ts.push(t); hit.push(x >= 0 && y >= 0 && x < W && y < H && inkD[y * W + x] > 0); }
198
+ return { ts, hit };
199
+ }
200
+ function refineLineUnused(ink, inkD, W, H, p0, d, nrm, med, tLo, tHi) {
201
+ let { ts, hit } = sampleLine(inkD, W, H, p0, d, tLo, tHi);
202
+ const band = Math.max(2, Math.floor(0.06 * med)), pts = [];
203
+ for (let i = 0; i < ts.length; i++) { if (!hit[i]) continue; const cx = p0[0] + d[0] * ts[i], cy = p0[1] + d[1] * ts[i];
204
+ for (let o = -band; o <= band; o++) { const x = Math.round(cx + nrm[0] * o), y = Math.round(cy + nrm[1] * o); if (x >= 0 && y >= 0 && x < W && y < H && ink[y * W + x]) pts.push([x, y]); } }
205
+ if (pts.length >= 10) {
206
+ const mx = pts.reduce((s, p) => s + p[0], 0) / pts.length, my = pts.reduce((s, p) => s + p[1], 0) / pts.length;
207
+ let a = 0, b = 0, c = 0; for (const p of pts) { const dx = p[0] - mx, dy = p[1] - my; a += dx * dx; b += dx * dy; c += dy * dy; }
208
+ const n1 = pts.length - 1; let d2 = eigen2(a / n1, b / n1, c / n1).evecs[0];
209
+ if (d2[0] * d[0] + d2[1] * d[1] < 0) d2 = [-d2[0], -d2[1]];
210
+ d = d2; p0 = [mx, my]; nrm = [-d[1], d[0]];
211
+ }
212
+ ({ ts, hit } = sampleLine(inkD, W, H, p0, d, tLo - med, tHi + med));
213
+ const idx = []; for (let i = 0; i < hit.length; i++) if (hit[i]) idx.push(i);
214
+ if (!idx.length) return null;
215
+ const gap = 0.5 * med, runs = []; let start = idx[0];
216
+ for (let k = 0; k + 1 < idx.length; k++) { if (ts[idx[k + 1]] - ts[idx[k]] > gap) { runs.push([start, idx[k]]); start = idx[k + 1]; } }
217
+ runs.push([start, idx[idx.length - 1]]);
218
+ let bestR = runs[0]; for (const r of runs) if (ts[r[1]] - ts[r[0]] > ts[bestR[1]] - ts[bestR[0]]) bestR = r;
219
+ const pa = [p0[0] + d[0] * ts[bestR[0]], p0[1] + d[1] * ts[bestR[0]]], pb = [p0[0] + d[0] * ts[bestR[1]], p0[1] + d[1] * ts[bestR[1]]];
220
+ if (Math.hypot(pb[0] - pa[0], pb[1] - pa[1]) < 1.2 * med) return null;
221
+ return { pa, pb, d, nrm };
222
+ }
223
+ function fillSmallHoles(ink, W, H, maxArea) {
224
+ const bg = new Uint8Array(W * H); for (let i = 0; i < bg.length; i++) bg[i] = ink[i] ? 0 : 1;
225
+ const lab = labelComponents(bg, W, H, 4), out = ink.slice();
226
+ for (let i = 0; i < out.length; i++) { const l = lab.labels[i]; if (l && !lab.stats[l].border && lab.stats[l].area < maxArea) out[i] = 1; }
227
+ return out;
228
+ }
229
+ function sampleBand(ink, W, H, p0, d, nrm, t0, t1, tol) {
230
+ const ts = [], hit = [];
231
+ for (let t = t0; t < t1; t += 1) {
232
+ const cx = p0[0] + d[0] * t, cy = p0[1] + d[1] * t; let h = false;
233
+ for (let o = -tol; o <= tol && !h; o++) { const x = Math.round(cx + nrm[0] * o), y = Math.round(cy + nrm[1] * o); if (x >= 0 && y >= 0 && x < W && y < H && ink[y * W + x]) h = true; }
234
+ ts.push(t); hit.push(h);
235
+ }
236
+ return { ts, hit };
237
+ }
238
+ function refineLine2(ink, inkD, inkLines, W, H, p0, d, nrm, med, tLo, tHi) {
239
+ let { ts, hit } = sampleLine(inkD, W, H, p0, d, tLo, tHi);
240
+ const band = Math.max(3, Math.floor(0.12 * med)), pts = [];
241
+ for (let i = 0; i < ts.length; i++) { if (!hit[i]) continue; const cx = p0[0] + d[0] * ts[i], cy = p0[1] + d[1] * ts[i];
242
+ for (let o = -band; o <= band; o++) { const x = Math.round(cx + nrm[0] * o), y = Math.round(cy + nrm[1] * o); if (x >= 0 && y >= 0 && x < W && y < H && ink[y * W + x]) pts.push([x, y]); } }
243
+ if (pts.length >= 10) {
244
+ const mx = pts.reduce((s, p) => s + p[0], 0) / pts.length, my = pts.reduce((s, p) => s + p[1], 0) / pts.length;
245
+ let a = 0, b = 0, c = 0; for (const p of pts) { const dx = p[0] - mx, dy = p[1] - my; a += dx * dx; b += dx * dy; c += dy * dy; }
246
+ const n1 = pts.length - 1; let d2 = eigen2(a / n1, b / n1, c / n1).evecs[0];
247
+ if (d2[0] * d[0] + d2[1] * d[1] < 0) d2 = [-d2[0], -d2[1]];
248
+ d = d2; p0 = [mx, my]; nrm = [-d[1], d[0]];
249
+ }
250
+ ({ ts, hit } = sampleBand(inkLines, W, H, p0, d, nrm, tLo - med, tHi + med, Math.max(3, Math.floor(0.1 * med))));
251
+ const idx = []; for (let i = 0; i < hit.length; i++) if (hit[i]) idx.push(i);
252
+ if (!idx.length) return null;
253
+ const gap = 0.5 * med, runs = []; let start = idx[0];
254
+ for (let k = 0; k + 1 < idx.length; k++) { if (ts[idx[k + 1]] - ts[idx[k]] > gap) { runs.push([start, idx[k]]); start = idx[k + 1]; } }
255
+ runs.push([start, idx[idx.length - 1]]);
256
+ let bestR = runs[0]; for (const r of runs) if (ts[r[1]] - ts[r[0]] > ts[bestR[1]] - ts[bestR[0]]) bestR = r;
257
+ const pa = [p0[0] + d[0] * ts[bestR[0]], p0[1] + d[1] * ts[bestR[0]]], pb = [p0[0] + d[0] * ts[bestR[1]], p0[1] + d[1] * ts[bestR[1]]];
258
+ if (Math.hypot(pb[0] - pa[0], pb[1] - pa[1]) < 1.2 * med) return null;
259
+ return { pa, pb, d, nrm };
260
+ }
261
+ function dotEvidence(ink0, W, H, pa, pb, med) {
262
+ const ink = fillSmallHoles(ink0, W, H, (0.35 * med) ** 2), dist = distanceTransform(ink, W, H);
263
+ const thick = (p, radius) => { const x = Math.round(p[0]), y = Math.round(p[1]), r = Math.floor(radius); let m = 0;
264
+ for (let yy = Math.max(0, y - r); yy <= Math.min(H - 1, y + r); yy++) for (let xx = Math.max(0, x - r); xx <= Math.min(W - 1, x + r); xx++) if (dist[yy * W + xx] > m) m = dist[yy * W + xx]; return m; };
265
+ const roundness = (p, radius) => { const x = Math.round(p[0]), y = Math.round(p[1]), r = Math.floor(radius); let m = 0, cx = x, cy = y;
266
+ for (let yy = Math.max(0, y - r); yy <= Math.min(H - 1, y + r); yy++) for (let xx = Math.max(0, x - r); xx <= Math.min(W - 1, x + r); xx++) if (dist[yy * W + xx] > m) { m = dist[yy * W + xx]; cx = xx; cy = yy; }
267
+ if (m <= 0) return 9; const k = Math.floor(1.6 * m) + 1; let s = 0;
268
+ for (let yy = Math.max(0, cy - k); yy <= Math.min(H - 1, cy + k); yy++) for (let xx = Math.max(0, cx - k); xx <= Math.min(W - 1, cx + k); xx++) s += ink[yy * W + xx];
269
+ return s / Math.max(1, Math.PI * m * m); };
270
+ const mids = []; for (let i = 0; i < 9; i++) { const t = 0.25 + 0.5 * i / 8; mids.push(thick([pa[0] + (pb[0] - pa[0]) * t, pa[1] + (pb[1] - pa[1]) * t], Math.max(2, 0.04 * med))); }
271
+ mids.sort((a, b) => a - b); const lineHalf = Math.max(1, mids[4]);
272
+ const L = Math.max(1e-6, Math.hypot(pb[0] - pa[0], pb[1] - pa[1])), u = [(pb[0] - pa[0]) / L, (pb[1] - pa[1]) / L];
273
+ const endScan = (p, dir) => { let best = 0, bp = p; const step = Math.max(1, 0.05 * med);
274
+ for (let t = -0.15 * med; t < 0.4 * med; t += step) { const q = [p[0] + dir[0] * t, p[1] + dir[1] * t], v = thick(q, Math.max(2, 0.12 * med)); if (v > best) { best = v; bp = q; } }
275
+ return [best, bp]; };
276
+ const [ta, qa] = endScan(pa, u), [tb, qb] = endScan(pb, [-u[0], -u[1]]);
277
+ const ra = ta / lineHalf, rb = tb / lineHalf, hi = Math.max(ra, rb), lo = Math.min(ra, rb);
278
+ const rd = roundness(ra > rb ? qa : qb, 0.12 * med);
279
+ if (hi >= 1.8 && lo <= 1.6 && hi >= 1.5 * lo && rd <= 3.5) return { start: ra > rb ? 0 : 1, strength: hi };
280
+ return { start: null, strength: hi };
281
+ }
282
+
283
+ export function findBaseline(ink, W, H, frames) {
284
+ if (!frames.length) return null;
285
+ const sizes = frames.map(f => f.size).sort((a, b) => a - b), med = sizes.length % 2 ? sizes[(sizes.length - 1) / 2] : (sizes[sizes.length / 2 - 1] + sizes[sizes.length / 2]) / 2;
286
+ const m = [frames.reduce((s, f) => s + f.center[0], 0) / frames.length, frames.reduce((s, f) => s + f.center[1], 0) / frames.length];
287
+ let d = rowAxis(frames), nrm = [-d[1], d[0]];
288
+ const along = frames.map(f => (f.center[0] - m[0]) * d[0] + (f.center[1] - m[1]) * d[1]).sort((a, b) => a - b);
289
+ // frames' ink masked out: their edges cannot pose as a baseline, and the underline is scored along the whole row
290
+ const frameMask = new Uint8Array(W * H);
291
+ for (const f of frames) { const mk = fillConvexFull(f.hull, W, H); for (let i = 0; i < mk.length; i++) if (mk[i]) frameMask[i] = 1; }
292
+ const strokes = frames.map(f => f.stroke).sort((a, b) => a - b), kd = Math.max(1, Math.floor((2 * strokes[strokes.length >> 1] + 4) / 2));
293
+ const fm = dilate(frameMask, W, H, kd);
294
+ const inkLines = new Uint8Array(W * H); for (let i = 0; i < inkLines.length; i++) inkLines[i] = ink[i] && !fm[i] ? 1 : 0;
295
+ const tLo = along[0] - 0.6 * med, tHi = along[along.length - 1] + 0.6 * med;
296
+ const inkD = dilate(inkLines, W, H, 2);
297
+ const offs = [], covs = [], nOff = Math.floor(4.8 * med / 2) + 1;
298
+ for (let k = 0; k < nOff; k++) {
299
+ const off = -2.4 * med + 4.8 * med * k / (nOff - 1); if (Math.abs(off) < 0.5 * med) continue;
300
+ const { ts, hit } = sampleLine(inkD, W, H, [m[0] + nrm[0] * off, m[1] + nrm[1] * off], d, tLo, tHi);
301
+ if (ts.length < 4) continue;
302
+ offs.push(off); covs.push(hit.filter(Boolean).length / ts.length);
303
+ }
304
+ if (!offs.length) return null;
305
+ const cands = [];
306
+ for (let i = 0; i < offs.length; i++) {
307
+ if (covs[i] < 0.3) continue;
308
+ let mx = 0; for (let j = Math.max(0, i - 3); j < Math.min(offs.length, i + 4); j++) mx = Math.max(mx, covs[j]);
309
+ if (covs[i] >= mx) { if (cands.length && Math.abs(offs[i] - cands[cands.length - 1][0]) < 0.2 * med) { if (covs[i] > cands[cands.length - 1][1]) cands[cands.length - 1] = [offs[i], covs[i]]; continue; } cands.push([offs[i], covs[i]]); }
310
+ }
311
+ if (!cands.length) return null;
312
+ const scored = [];
313
+ for (const [off, cov] of cands) {
314
+ const r = refineLine2(ink, inkD, inkLines, W, H, [m[0] + nrm[0] * off, m[1] + nrm[1] * off], d, nrm, med, tLo, tHi); if (!r) continue;
315
+ const { start } = dotEvidence(inkLines, W, H, r.pa, r.pb, med);
316
+ const near = Math.abs(off) / med >= 0.5 && Math.abs(off) / med <= 1.5;
317
+ scored.push({ key: [near ? 1 : 0, start !== null ? 1 : 0, -Math.round(Math.abs(off) / med * 10) / 10, cov], off, cov, ...r, start });
318
+ }
319
+ if (!scored.length) return null;
320
+ scored.sort((a, b) => { for (let i = 0; i < 4; i++) if (a.key[i] !== b.key[i]) return b.key[i] - a.key[i]; return 0; });
321
+ let { pa, pb, d: dd, nrm: nn, start } = scored[0];
322
+ let sideSum = 0; for (const f of frames) sideSum += (f.center[0] - pa[0]) * nn[0] + (f.center[1] - pa[1]) * nn[1];
323
+ const side = Math.sign(sideSum / frames.length) || 1, up = [nn[0] * side, nn[1] * side];
324
+ if (start === 0) return { pStart: pa, pEnd: pb, startKnown: true, up };
325
+ if (start === 1) return { pStart: pb, pEnd: pa, startKnown: true, up };
326
+ if (dd[0] < 0 || (dd[0] === 0 && dd[1] < 0)) [pa, pb] = [pb, pa];
327
+ return { pStart: pa, pEnd: pb, startKnown: false, up };
328
+ }
329
+
330
+ function fillConvexFull(poly, W, H) {
331
+ const mask = new Uint8Array(W * H), n = poly.length;
332
+ let y0 = Infinity, y1 = -Infinity; for (const p of poly) { if (p[1] < y0) y0 = p[1]; if (p[1] > y1) y1 = p[1]; }
333
+ for (let y = Math.max(0, Math.floor(y0)); y <= Math.min(H - 1, Math.ceil(y1)); y++) {
334
+ const yy = y + 0.5; let xl = Infinity, xr = -Infinity;
335
+ for (let i = 0; i < n; i++) { const [ax, ay] = poly[i], [bx, by] = poly[(i + 1) % n]; if ((ay <= yy && by > yy) || (by <= yy && ay > yy)) { const x = ax + (yy - ay) * (bx - ax) / (by - ay); if (x < xl) xl = x; if (x > xr) xr = x; } }
336
+ if (xl === Infinity) continue;
337
+ for (let x = Math.max(0, Math.ceil(xl - 0.5)); x <= Math.min(W - 1, Math.floor(xr - 0.5)); x++) mask[y * W + x] = 1;
338
+ }
339
+ return mask;
340
+ }
341
+
342
+ function targetQuad(size = PATCH, margin = PATCH_MARGIN) { const m = size * margin; return [[m, m], [size - m, m], [size - m, size - m], [m, size - m]]; }
343
+ function orderCorners(corners, center, d, up) {
344
+ const u = corners.map(c => (c[0] - center[0]) * d[0] + (c[1] - center[1]) * d[1]), v = corners.map(c => (c[0] - center[0]) * up[0] + (c[1] - center[1]) * up[1]);
345
+ const scores = corners.map((c, i) => [-u[i] + v[i], u[i] + v[i], u[i] - v[i], -u[i] - v[i]]);
346
+ const order = [], taken = new Set();
347
+ for (let k = 0; k < 4; k++) { let bi = -1; for (let i = 0; i < 4; i++) if (!taken.has(i) && (bi < 0 || scores[i][k] > scores[bi][k])) bi = i; order.push(bi); taken.add(bi); }
348
+ return order.map(i => corners[i]);
349
+ }
350
+ function rot3(t) { return Float64Array.from([Math.cos(t), -Math.sin(t), 0, Math.sin(t), Math.cos(t), 0, 0, 0, 1]); }
351
+ function circleMatrix(e, d, size = PATCH) {
352
+ const a = e.ang * Math.PI / 180, T = Float64Array.from([1, 0, -e.cx, 0, 1, -e.cy, 0, 0, 1]), S = Float64Array.from([2 / Math.max(e.MA, 1e-6), 0, 0, 0, 2 / Math.max(e.ma, 1e-6), 0, 0, 0, 1]);
353
+ let N = mat3mul(S, mat3mul(rot3(-a), T));
354
+ const ddx = N[0] * d[0] + N[1] * d[1], ddy = N[3] * d[0] + N[4] * d[1], th = Math.atan2(ddy, ddx);
355
+ N = mat3mul(rot3(-th), N);
356
+ const P = homography([[-1, -1], [1, -1], [1, 1], [-1, 1]], targetQuad(size));
357
+ return mat3mul(P, N);
358
+ }
359
+ export function rectifyFrame(gray, W, H, f, d, up, lightInk) {
360
+ let patch;
361
+ if (f.kind === FRAME_SQUARE && f.corners) {
362
+ const src = orderCorners(f.corners, f.center, d, up), M = homography(targetQuad(), src); // dst -> src
363
+ patch = warp(gray, W, H, M, PATCH, PATCH);
364
+ } else if (f.ellipse) {
365
+ const M = circleMatrix(f.ellipse, d);
366
+ patch = warp(gray, W, H, mat3inv(M), PATCH, PATCH);
367
+ const uu = [M[0] * up[0] + M[1] * up[1], M[3] * up[0] + M[4] * up[1]];
368
+ if (uu[1] > 0) { const fl = new Float32Array(PATCH * PATCH); for (let y = 0; y < PATCH; y++) fl.set(patch.subarray((PATCH - 1 - y) * PATCH, (PATCH - y) * PATCH), y * PATCH); patch = fl; }
369
+ } else {
370
+ const h = f.size / 2, c = f.center, corners = [[c[0] - h, c[1] - h], [c[0] + h, c[1] - h], [c[0] + h, c[1] + h], [c[0] - h, c[1] + h]];
371
+ patch = warp(gray, W, H, homography(targetQuad(), orderCorners(corners, c, d, up)), PATCH, PATCH);
372
+ }
373
+ return normalizePatch(patch, PATCH, lightInk);
374
+ }
375
+
376
+ export function detect(grayIn, Win, Hin, junkFn = null) {
377
+ const { gray, W, H, scale } = prepare(grayIn, Win, Hin);
378
+ const inks = {}; let pool = [];
379
+ for (const light of [false, true]) {
380
+ const ink = binarize(gray, W, H, light); inks[light] = ink;
381
+ let cov = 0; for (let i = 0; i < ink.length; i++) cov += ink[i]; cov /= ink.length;
382
+ const penalty = 1 - Math.min(0.9, 3 * Math.max(0, cov - 0.2));
383
+ for (const f of findFrames(ink, W, H)) { f.lightInk = light; f.quality *= penalty; pool.push(f); }
384
+ }
385
+ pool.sort((a, b) => b.quality - a.quality);
386
+ let merged = [];
387
+ for (const f of pool) { if (merged.some(g => Math.hypot(f.center[0] - g.center[0], f.center[1] - g.center[1]) < 0.35 * g.size && f.size / g.size > 0.6 && f.size / g.size < 1.6)) continue; merged.push(f); }
388
+ if (junkFn && merged.length) {
389
+ // the network is the expensive part: ask it only about the best-looking candidates
390
+ const top = merged.slice(0, JUNK_TOP_K);
391
+ const pj = junkFn(top.map(f => rectifyFrame(gray, W, H, f, [1, 0], [0, -1], f.lightInk)));
392
+ top.forEach((f, i) => { f.junk = pj[i]; f.quality *= Math.max(0.05, 1 - pj[i]); });
393
+ merged = top.filter(f => f.junk < 0.9);
394
+ }
395
+ let { frames, warnings } = filterRow(merged);
396
+ const votes = frames.reduce((s, f) => s + (f.lightInk ? 1 : -1), 0), lightInk = votes > 0, ink = inks[lightInk];
397
+ let up = [0, -1], d = [1, 0], baseline = null, startKnown = false;
398
+ const bl = findBaseline(ink, W, H, frames);
399
+ if (bl) {
400
+ startKnown = bl.startKnown; up = bl.up;
401
+ const L = Math.hypot(bl.pEnd[0] - bl.pStart[0], bl.pEnd[1] - bl.pStart[1]) || 1e-6;
402
+ d = [(bl.pEnd[0] - bl.pStart[0]) / L, (bl.pEnd[1] - bl.pStart[1]) / L]; baseline = [bl.pStart, bl.pEnd];
403
+ if (!startKnown) warnings.push('baseline found but no start dot: reading left to right');
404
+ } else warnings.push('no baseline: assuming the photo is upright and reads left to right');
405
+ if (frames.length) {
406
+ if (bl) {
407
+ const sizes = frames.map(f => f.size).sort((a, b) => a - b), med = sizes[sizes.length >> 1];
408
+ const length = Math.hypot(baseline[1][0] - baseline[0][0], baseline[1][1] - baseline[0][1]);
409
+ const proj = frames.map(f => (f.center[0] - baseline[0][0]) * d[0] + (f.center[1] - baseline[0][1]) * d[1]);
410
+ const span = frames.length > 1 ? Math.max(...proj) - Math.min(...proj) : 0;
411
+ if (length >= 0.8 * span) { const kept = frames.filter((f, i) => proj[i] >= -0.8 * med && proj[i] <= length + 0.8 * med); if (kept.length && kept.length < frames.length) { warnings.push(`dropped ${frames.length - kept.length} frame(s) beyond the baseline`); frames = kept; } }
412
+ frames.sort((a, b) => ((a.center[0] - baseline[0][0]) * d[0] + (a.center[1] - baseline[0][1]) * d[1]) - ((b.center[0] - baseline[0][0]) * d[0] + (b.center[1] - baseline[0][1]) * d[1]));
413
+ } else frames.sort((a, b) => a.center[0] - b.center[0]);
414
+ }
415
+ const patches = frames.map(f => rectifyFrame(gray, W, H, f, d, up, lightInk));
416
+ return { frames, baseline, startKnown, up, direction: d, lightInk, scale, W, H, warnings, patches };
417
+ }
package/src/geom.js ADDED
@@ -0,0 +1,94 @@
1
+ // Geometry helpers. Points are [x, y].
2
+
3
+ export function convexHull(pts) {
4
+ const p = pts.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
5
+ if (p.length < 3) return p;
6
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
7
+ const lo = [], up = [];
8
+ for (const q of p) { while (lo.length >= 2 && cross(lo[lo.length - 2], lo[lo.length - 1], q) <= 0) lo.pop(); lo.push(q); }
9
+ for (let i = p.length - 1; i >= 0; i--) { const q = p[i]; while (up.length >= 2 && cross(up[up.length - 2], up[up.length - 1], q) <= 0) up.pop(); up.push(q); }
10
+ lo.pop(); up.pop();
11
+ return lo.concat(up);
12
+ }
13
+ export function polygonArea(poly) { let a = 0; for (let i = 0, n = poly.length; i < n; i++) { const p = poly[i], q = poly[(i + 1) % n]; a += p[0] * q[1] - q[0] * p[1]; } return Math.abs(a) / 2; }
14
+ export function polygonPerimeter(poly) { let s = 0; for (let i = 0, n = poly.length; i < n; i++) { const p = poly[i], q = poly[(i + 1) % n]; s += Math.hypot(q[0] - p[0], q[1] - p[1]); } return s; }
15
+ export function polygonCentroid(poly) {
16
+ let a = 0, cx = 0, cy = 0;
17
+ for (let i = 0, n = poly.length; i < n; i++) { const p = poly[i], q = poly[(i + 1) % n], c = p[0] * q[1] - q[0] * p[1]; a += c; cx += (p[0] + q[0]) * c; cy += (p[1] + q[1]) * c; }
18
+ if (Math.abs(a) < 1e-9) { const n = poly.length; return [poly.reduce((s, p) => s + p[0], 0) / n, poly.reduce((s, p) => s + p[1], 0) / n]; }
19
+ return [cx / (3 * a), cy / (3 * a)];
20
+ }
21
+ export function pointInPolygon(poly, x, y) {
22
+ let inside = false;
23
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
24
+ const xi = poly[i][0], yi = poly[i][1], xj = poly[j][0], yj = poly[j][1];
25
+ if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
26
+ }
27
+ return inside;
28
+ }
29
+ function rdp(pts, eps) {
30
+ if (pts.length < 3) return pts;
31
+ const a = pts[0], b = pts[pts.length - 1]; let idx = -1, dmax = 0;
32
+ const L = Math.hypot(b[0] - a[0], b[1] - a[1]);
33
+ for (let i = 1; i < pts.length - 1; i++) {
34
+ const p = pts[i];
35
+ const d = L < 1e-9 ? Math.hypot(p[0] - a[0], p[1] - a[1]) : Math.abs((b[0] - a[0]) * (a[1] - p[1]) - (a[0] - p[0]) * (b[1] - a[1])) / L;
36
+ if (d > dmax) { dmax = d; idx = i; }
37
+ }
38
+ if (dmax > eps) { const l = rdp(pts.slice(0, idx + 1), eps), r = rdp(pts.slice(idx), eps); return l.slice(0, -1).concat(r); }
39
+ return [a, b];
40
+ }
41
+ // closed-polygon simplification: split at the two farthest points, simplify both arcs
42
+ export function approxPolyDP(poly, eps) {
43
+ if (poly.length < 4) return poly;
44
+ let i0 = 0, i1 = 0, dmax = -1;
45
+ for (let i = 0; i < poly.length; i++) { const d = Math.hypot(poly[i][0] - poly[0][0], poly[i][1] - poly[0][1]); if (d > dmax) { dmax = d; i1 = i; } }
46
+ const arc1 = poly.slice(i0, i1 + 1), arc2 = poly.slice(i1).concat([poly[0]]);
47
+ const s1 = rdp(arc1, eps), s2 = rdp(arc2, eps);
48
+ return s1.slice(0, -1).concat(s2.slice(0, -1));
49
+ }
50
+ function quadArea(q) { return Math.abs((q[0][0] * q[1][1] - q[1][0] * q[0][1]) + (q[1][0] * q[2][1] - q[2][0] * q[1][1]) + (q[2][0] * q[3][1] - q[3][0] * q[2][1]) + (q[3][0] * q[0][1] - q[0][0] * q[3][1])) / 2; }
51
+ export function maxInscribedQuad(hull) {
52
+ const peri = polygonPerimeter(hull);
53
+ let pts = approxPolyDP(hull, 0.012 * peri);
54
+ if (pts.length > 14) { const idx = []; for (let i = 0; i < 14; i++) idx.push(Math.round(i * (pts.length - 1) / 13)); pts = idx.map(i => pts[i]); }
55
+ if (pts.length < 4) return { quad: null, area: 0 };
56
+ let best = null, bestA = 0; const n = pts.length;
57
+ for (let a = 0; a < n; a++) for (let b = a + 1; b < n; b++) for (let c = b + 1; c < n; c++) for (let d = c + 1; d < n; d++) {
58
+ const q = [pts[a], pts[b], pts[c], pts[d]], ar = quadArea(q); if (ar > bestA) { bestA = ar; best = q; }
59
+ }
60
+ return { quad: best, area: bestA };
61
+ }
62
+ // symmetric 2x2 eigen decomposition: returns {evals:[l1,l2] (l1>=l2), evecs:[[x,y],[x,y]]}
63
+ export function eigen2(a, b, d) {
64
+ const tr = a + d, det = a * d - b * b, disc = Math.sqrt(Math.max(0, tr * tr / 4 - det));
65
+ const l1 = tr / 2 + disc, l2 = tr / 2 - disc;
66
+ let v1 = Math.abs(b) > 1e-12 ? [l1 - d, b] : (a >= d ? [1, 0] : [0, 1]);
67
+ const n = Math.hypot(v1[0], v1[1]) || 1; v1 = [v1[0] / n, v1[1] / n];
68
+ return { evals: [l1, l2], evecs: [v1, [-v1[1], v1[0]]] };
69
+ }
70
+ function solve(A, b) { // Gaussian elimination with partial pivoting, A n x n (array of rows)
71
+ const n = b.length, M = A.map((r, i) => r.concat([b[i]]));
72
+ for (let c = 0; c < n; c++) {
73
+ let p = c; for (let r = c + 1; r < n; r++) if (Math.abs(M[r][c]) > Math.abs(M[p][c])) p = r;
74
+ [M[c], M[p]] = [M[p], M[c]];
75
+ const piv = M[c][c] || 1e-12;
76
+ for (let r = 0; r < n; r++) { if (r === c) continue; const f = M[r][c] / piv; for (let k = c; k <= n; k++) M[r][k] -= f * M[c][k]; }
77
+ }
78
+ return M.map((r, i) => r[n] / (r[i] || 1e-12));
79
+ }
80
+ // 3x3 homography mapping src[i] -> dst[i] (4 points each), row-major Float64Array(9)
81
+ export function homography(src, dst) {
82
+ const A = [], b = [];
83
+ for (let i = 0; i < 4; i++) {
84
+ const [x, y] = src[i], [u, v] = dst[i];
85
+ A.push([x, y, 1, 0, 0, 0, -u * x, -u * y]); b.push(u);
86
+ A.push([0, 0, 0, x, y, 1, -v * x, -v * y]); b.push(v);
87
+ }
88
+ const h = solve(A, b); return Float64Array.from([h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1]);
89
+ }
90
+ export function mat3mul(A, B) { const C = new Float64Array(9); for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) { let s = 0; for (let k = 0; k < 3; k++) s += A[i * 3 + k] * B[k * 3 + j]; C[i * 3 + j] = s; } return C; }
91
+ export function mat3inv(M) {
92
+ const [a, b, c, d, e, f, g, h, i] = M, A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g, det = a * A + b * B + c * C || 1e-12;
93
+ return Float64Array.from([A, -(b * i - c * h), b * f - c * e, B, a * i - c * g, -(a * f - c * d), C, -(a * h - b * g), a * e - b * d].map(v => v / det));
94
+ }
package/src/imgops.js ADDED
@@ -0,0 +1,189 @@
1
+ // Image primitives for the glyphbyte detector, plain JavaScript, no dependencies.
2
+ // Images are row-major typed arrays; binary images are Uint8Array with 1 = ink.
3
+
4
+ export function toGray(rgba, W, H) {
5
+ const g = new Uint8Array(W * H);
6
+ for (let i = 0, j = 0; i < g.length; i++, j += 4) g[i] = (0.299 * rgba[j] + 0.587 * rgba[j + 1] + 0.114 * rgba[j + 2] + 0.5) | 0;
7
+ return g;
8
+ }
9
+
10
+ export function integral(img, W, H) {
11
+ const S = new Float64Array((W + 1) * (H + 1));
12
+ for (let y = 1; y <= H; y++) {
13
+ let row = 0;
14
+ for (let x = 1; x <= W; x++) { row += img[(y - 1) * W + (x - 1)]; S[y * (W + 1) + x] = S[(y - 1) * (W + 1) + x] + row; }
15
+ }
16
+ return S;
17
+ }
18
+
19
+ function boxSum(S, W, x0, y0, x1, y1) { // inclusive-exclusive box on an integral image
20
+ const w = W + 1;
21
+ return S[y1 * w + x1] - S[y0 * w + x1] - S[y1 * w + x0] + S[y0 * w + x0];
22
+ }
23
+
24
+ // area-averaging downscale (like INTER_AREA), then bilinear for any residual
25
+ export function resizeGray(g, W, H, nw, nh) {
26
+ const S = integral(g, W, H), out = new Uint8Array(nw * nh);
27
+ for (let y = 0; y < nh; y++) {
28
+ const y0 = Math.floor(y * H / nh), y1 = Math.max(y0 + 1, Math.floor((y + 1) * H / nh));
29
+ for (let x = 0; x < nw; x++) {
30
+ const x0 = Math.floor(x * W / nw), x1 = Math.max(x0 + 1, Math.floor((x + 1) * W / nw));
31
+ out[y * nw + x] = (boxSum(S, W, x0, y0, x1, y1) / ((x1 - x0) * (y1 - y0)) + 0.5) | 0;
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function boxMean(img, W, H, r) {
38
+ const S = integral(img, W, H), out = new Float32Array(W * H);
39
+ for (let y = 0; y < H; y++) {
40
+ const y0 = Math.max(0, y - r), y1 = Math.min(H, y + r + 1);
41
+ for (let x = 0; x < W; x++) {
42
+ const x0 = Math.max(0, x - r), x1 = Math.min(W, x + r + 1);
43
+ out[y * W + x] = boxSum(S, W, x0, y0, x1, y1) / ((x1 - x0) * (y1 - y0));
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ // ink = src <= localMean - C. OpenCV's ADAPTIVE_THRESH_GAUSSIAN_C weights the block window with a
50
+ // Gaussian of sigma 0.3*((block-1)*0.5-1)+0.8; three box passes of the matching radius approximate it.
51
+ export function adaptiveThreshold(gray, W, H, block, C, invert) {
52
+ const src = invert ? gray.map(v => 255 - v) : gray;
53
+ const sigma = 0.3 * ((block - 1) * 0.5 - 1) + 0.8, r = Math.max(1, Math.round(Math.sqrt(sigma * sigma * 12 / 3 + 1) / 2 - 0.5));
54
+ let m = boxMean(src, W, H, r); m = boxMean(m, W, H, r); m = boxMean(m, W, H, r);
55
+ const out = new Uint8Array(W * H);
56
+ for (let i = 0; i < out.length; i++) out[i] = src[i] <= m[i] - C ? 1 : 0;
57
+ return out;
58
+ }
59
+
60
+ function dilatePlus(b, W, H) {
61
+ const o = new Uint8Array(W * H);
62
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
63
+ const i = y * W + x;
64
+ if (b[i] || (x > 0 && b[i - 1]) || (x < W - 1 && b[i + 1]) || (y > 0 && b[i - W]) || (y < H - 1 && b[i + W])) o[i] = 1;
65
+ }
66
+ return o;
67
+ }
68
+ function erodePlus(b, W, H) {
69
+ const o = new Uint8Array(W * H);
70
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
71
+ const i = y * W + x;
72
+ if (b[i] && (x === 0 || b[i - 1]) && (x === W - 1 || b[i + 1]) && (y === 0 || b[i - W]) && (y === H - 1 || b[i + W])) o[i] = 1;
73
+ }
74
+ return o;
75
+ }
76
+ export function morphClose3(b, W, H) { return erodePlus(dilatePlus(b, W, H), W, H); }
77
+ export function dilate(b, W, H, r) { // square dilation radius r
78
+ const o = new Uint8Array(W * H);
79
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
80
+ if (!b[y * W + x]) continue;
81
+ for (let dy = -r; dy <= r; dy++) { const yy = y + dy; if (yy < 0 || yy >= H) continue;
82
+ for (let dx = -r; dx <= r; dx++) { const xx = x + dx; if (xx >= 0 && xx < W) o[yy * W + xx] = 1; } }
83
+ }
84
+ return o;
85
+ }
86
+
87
+ // connected components; connectivity 4 or 8. Returns labels (Int32Array, 0 = background) and stats.
88
+ export function labelComponents(bin, W, H, conn = 8) {
89
+ const labels = new Int32Array(W * H);
90
+ const parent = [0];
91
+ const find = (a) => { while (parent[a] !== a) { parent[a] = parent[parent[a]]; a = parent[a]; } return a; };
92
+ const union = (a, b) => { a = find(a); b = find(b); if (a !== b) { if (a < b) parent[b] = a; else parent[a] = b; } };
93
+ let next = 1;
94
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
95
+ const i = y * W + x;
96
+ if (!bin[i]) continue;
97
+ let l = 0;
98
+ const up = y > 0 ? labels[i - W] : 0;
99
+ const left = x > 0 ? labels[i - 1] : 0;
100
+ const ul = conn === 8 && y > 0 && x > 0 ? labels[i - W - 1] : 0;
101
+ const ur = conn === 8 && y > 0 && x < W - 1 ? labels[i - W + 1] : 0;
102
+ if (up) l = up;
103
+ if (left) { if (!l) l = left; else if (left !== l) union(l, left); }
104
+ if (ul) { if (!l) l = ul; else if (ul !== l) union(l, ul); }
105
+ if (ur) { if (!l) l = ur; else if (ur !== l) union(l, ur); }
106
+ if (!l) { l = next++; parent.push(l); }
107
+ labels[i] = l;
108
+ }
109
+ const remap = new Int32Array(next); let n = 0;
110
+ for (let l = 1; l < next; l++) { const r = find(l); if (!remap[r]) remap[r] = ++n; remap[l] = remap[r]; }
111
+ const stats = []; for (let k = 0; k <= n; k++) stats.push({ area: 0, x0: W, y0: H, x1: -1, y1: -1, border: false });
112
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
113
+ const i = y * W + x; if (!labels[i]) continue;
114
+ const l = remap[labels[i]]; labels[i] = l; const s = stats[l];
115
+ s.area++; if (x < s.x0) s.x0 = x; if (y < s.y0) s.y0 = y; if (x > s.x1) s.x1 = x; if (y > s.y1) s.y1 = y;
116
+ if (x === 0 || y === 0 || x === W - 1 || y === H - 1) s.border = true;
117
+ }
118
+ return { labels, n, stats };
119
+ }
120
+
121
+ export function removeSpecks(ink, W, H, minArea) {
122
+ const { labels, stats } = labelComponents(ink, W, H, 8);
123
+ const out = new Uint8Array(W * H);
124
+ for (let i = 0; i < out.length; i++) if (labels[i] && stats[labels[i]].area >= minArea) out[i] = 1;
125
+ return out;
126
+ }
127
+
128
+ // chamfer distance transform (distance from ink pixels to the nearest background), OpenCV's 3x3 L2 weights
129
+ export function distanceTransform(bin, W, H) {
130
+ const a = 0.955, b = 1.3693, INF = 1e9, d = new Float32Array(W * H);
131
+ for (let i = 0; i < d.length; i++) d[i] = bin[i] ? INF : 0;
132
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
133
+ const i = y * W + x; if (!d[i]) continue; let v = d[i];
134
+ if (x > 0) v = Math.min(v, d[i - 1] + a);
135
+ if (y > 0) { v = Math.min(v, d[i - W] + a); if (x > 0) v = Math.min(v, d[i - W - 1] + b); if (x < W - 1) v = Math.min(v, d[i - W + 1] + b); }
136
+ d[i] = v;
137
+ }
138
+ for (let y = H - 1; y >= 0; y--) for (let x = W - 1; x >= 0; x--) {
139
+ const i = y * W + x; if (!d[i]) continue; let v = d[i];
140
+ if (x < W - 1) v = Math.min(v, d[i + 1] + a);
141
+ if (y < H - 1) { v = Math.min(v, d[i + W] + a); if (x < W - 1) v = Math.min(v, d[i + W + 1] + b); if (x > 0) v = Math.min(v, d[i + W - 1] + b); }
142
+ d[i] = v;
143
+ }
144
+ return d;
145
+ }
146
+
147
+ function gaussKernel(sigma) {
148
+ const k = (Math.round(sigma * 6 + 1) | 1), r = k >> 1, w = new Float64Array(k); let s = 0;
149
+ for (let i = 0; i < k; i++) { w[i] = Math.exp(-((i - r) ** 2) / (2 * sigma * sigma)); s += w[i]; }
150
+ for (let i = 0; i < k; i++) w[i] /= s;
151
+ return w;
152
+ }
153
+ function reflect101(i, n) { while (i < 0 || i >= n) { if (i < 0) i = -i; if (i >= n) i = 2 * n - 2 - i; } return i; }
154
+ export function gaussianBlur(img, W, H, sigma) {
155
+ const w = gaussKernel(sigma), r = w.length >> 1, tmp = new Float32Array(W * H), out = new Float32Array(W * H);
156
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { let s = 0; for (let k = -r; k <= r; k++) s += img[y * W + reflect101(x + k, W)] * w[k + r]; tmp[y * W + x] = s; }
157
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { let s = 0; for (let k = -r; k <= r; k++) s += tmp[reflect101(y + k, H) * W + x] * w[k + r]; out[y * W + x] = s; }
158
+ return out;
159
+ }
160
+
161
+ // sample src (gray) at dst pixels through the 3x3 matrix M (dst -> src), bilinear, border replicate
162
+ export function warp(gray, W, H, M, ow, oh) {
163
+ const out = new Float32Array(ow * oh), offs = [-0.25, 0.25];
164
+ const sample = (px, py) => {
165
+ const den = M[6] * px + M[7] * py + M[8];
166
+ let sx = (M[0] * px + M[1] * py + M[2]) / den, sy = (M[3] * px + M[4] * py + M[5]) / den;
167
+ sx = Math.min(Math.max(sx, 0), W - 1); sy = Math.min(Math.max(sy, 0), H - 1);
168
+ const x0 = Math.floor(sx), y0 = Math.floor(sy), x1 = Math.min(x0 + 1, W - 1), y1 = Math.min(y0 + 1, H - 1), fx = sx - x0, fy = sy - y0;
169
+ return (gray[y0 * W + x0] * (1 - fx) + gray[y0 * W + x1] * fx) * (1 - fy) + (gray[y1 * W + x0] * (1 - fx) + gray[y1 * W + x1] * fx) * fy;
170
+ };
171
+ for (let y = 0; y < oh; y++) for (let x = 0; x < ow; x++) {
172
+ let s = 0; for (const dy of offs) for (const dx of offs) s += sample(x + dx, y + dy);
173
+ out[y * ow + x] = s / 4;
174
+ }
175
+ return out;
176
+ }
177
+
178
+ // same maths as glyphbyte.synth.normalize_patch
179
+ export function normalizePatch(p, size, lightInk) {
180
+ const bg = gaussianBlur(p, size, size, size / 6), d = new Float32Array(p.length);
181
+ for (let i = 0; i < d.length; i++) d[i] = lightInk ? bg[i] - p[i] : p[i] - bg[i];
182
+ const d2 = new Float32Array(d.length); for (let i = 0; i < d.length; i++) d2[i] = d[i] * d[i];
183
+ const v = gaussianBlur(d2, size, size, size / 4), out = new Float32Array(d.length);
184
+ for (let i = 0; i < d.length; i++) {
185
+ const n = Math.min(3, Math.max(-3, d[i] / (Math.sqrt(v[i]) + 4)));
186
+ out[i] = Math.floor((n + 3) / 6 * 255) / 255; // uint8 truncation, then /255 as the classifier expects
187
+ }
188
+ return out;
189
+ }
package/src/index.js ADDED
@@ -0,0 +1,24 @@
1
+ // glyphbyte: one drawn glyph is one byte. Public API of the JavaScript implementation.
2
+ export { SYMBOLS, unpack, describe, readImage } from './pipeline.js';
3
+ export { detect } from './detect.js';
4
+ export { toGray } from './imgops.js';
5
+ import { loadWeights } from './nn.js';
6
+ export { loadWeights, classify } from './nn.js';
7
+ export { drawRow, drawSheet, drawCell, drawDetection } from './render.js';
8
+ export { lookup, buildFilters, matchPrefix, fetchProfiles, probeRelay } from './lookup.js';
9
+ export { npub, note, nevent, naddr, decodeEntity, bech32Encode, bech32Decode, isAddressable } from './nip19.js';
10
+
11
+ export function pack(symbol, rotation, fill, frame) { return (symbol << 4) | (rotation << 2) | (fill << 1) | frame; }
12
+
13
+ // The bundled small model. In Node this reads the package files; in a browser pass your own
14
+ // bytes to loadWeights (for example fetched from your own origin) or use the single-file napplet.
15
+ export async function loadBundledModel() {
16
+ const [{ readFile }, { fileURLToPath }] = await Promise.all([import('node:fs/promises'), import('node:url')]);
17
+ const dir = fileURLToPath(new URL('..', import.meta.url));
18
+ const manifest = JSON.parse(await readFile(dir + '/weights.bin.json', 'utf8'));
19
+ return loadWeights(manifest, new Uint8Array(await readFile(dir + '/weights.bin')));
20
+ }
21
+ export async function loadShapes() {
22
+ const { readFile } = await import('node:fs/promises'); const { fileURLToPath } = await import('node:url');
23
+ return JSON.parse(await readFile(fileURLToPath(new URL('../shapes.json', import.meta.url)), 'utf8'));
24
+ }
package/src/lookup.js ADDED
@@ -0,0 +1,95 @@
1
+ // Turn candidate prefixes into relay queries. Inside a NIP-5D shell the page has no network
2
+ // and must use window.napplet.relay; as a standalone file it opens a WebSocket itself.
3
+
4
+ export function buildFilters(prefixes, kinds0Only = true) {
5
+ const ps = Array.from(new Set(prefixes.map(p => p.toLowerCase()).filter(p => /^[0-9a-f]{2,64}$/.test(p))));
6
+ if (!ps.length) return [];
7
+ const f = [{ ids: ps }, { authors: ps, kinds: [0] }];
8
+ if (!kinds0Only) f.push({ authors: ps, limit: 20 });
9
+ return f;
10
+ }
11
+
12
+ function viaWebSocket(url, filters, timeoutMs) {
13
+ return new Promise(resolve => {
14
+ const events = [], seen = new Set(); let notices = [], done = false, ws;
15
+ const finish = (error) => { if (done) return; done = true; try { ws && ws.close(); } catch (e) { /* ignore */ } resolve({ events, notices, error, via: 'websocket' }); };
16
+ try { ws = new WebSocket(url); } catch (e) { return finish(String(e)); }
17
+ const timer = setTimeout(() => finish('timeout'), timeoutMs);
18
+ ws.onopen = () => ws.send(JSON.stringify(['REQ', 'glyphbyte', ...filters]));
19
+ ws.onmessage = m => {
20
+ let msg; try { msg = JSON.parse(m.data); } catch (e) { return; }
21
+ if (msg[0] === 'EVENT' && msg[2] && !seen.has(msg[2].id)) { seen.add(msg[2].id); events.push(msg[2]); }
22
+ else if (msg[0] === 'EOSE') { clearTimeout(timer); ws.send(JSON.stringify(['CLOSE', 'glyphbyte'])); finish(undefined); }
23
+ else if (msg[0] === 'NOTICE') notices.push(String(msg[1]));
24
+ else if (msg[0] === 'CLOSED') { notices.push('CLOSED: ' + msg[2]); clearTimeout(timer); finish(undefined); }
25
+ };
26
+ ws.onerror = () => { clearTimeout(timer); finish('connection failed'); };
27
+ ws.onclose = () => { clearTimeout(timer); finish(undefined); };
28
+ });
29
+ }
30
+
31
+ async function viaNapplet(relayApi, url, filters, timeoutMs) {
32
+ // NAP-RELAY: query(filters) on the shell's pool; subscribe(filters, {relay}) can target one relay
33
+ try {
34
+ if (typeof relayApi.subscribe === 'function' && url) {
35
+ return await new Promise(resolve => {
36
+ const events = [], seen = new Set(); let sub;
37
+ const timer = setTimeout(() => { try { sub && sub.close && sub.close(); } catch (e) { /* ignore */ } resolve({ events, notices: [], error: 'timeout', via: 'napplet' }); }, timeoutMs);
38
+ try {
39
+ sub = relayApi.subscribe(filters, { relay: url });
40
+ const onEvent = r => { const ev = r && r.event ? r.event : r; if (ev && ev.id && !seen.has(ev.id)) { seen.add(ev.id); events.push(ev); } };
41
+ const onEose = () => { clearTimeout(timer); try { sub.close && sub.close(); } catch (e) { /* ignore */ } resolve({ events, notices: [], via: 'napplet' }); };
42
+ if (typeof sub.on === 'function') { sub.on('event', onEvent); sub.on('eose', onEose); }
43
+ else if (typeof sub.addEventListener === 'function') { sub.addEventListener('event', e => onEvent(e.detail || e)); sub.addEventListener('eose', onEose); }
44
+ else if (sub && typeof sub.then === 'function') { sub.then(res => { (res || []).forEach(onEvent); onEose(); }); }
45
+ else { sub.onevent = onEvent; sub.oneose = onEose; }
46
+ } catch (e) { clearTimeout(timer); resolve({ events: [], notices: [], error: String(e), via: 'napplet' }); }
47
+ });
48
+ }
49
+ const res = await relayApi.query(filters);
50
+ const events = (res || []).map(r => (r && r.event) ? r.event : r).filter(e => e && e.id);
51
+ return { events, notices: [], via: 'napplet' };
52
+ } catch (e) { return { events: [], notices: [], error: String(e), via: 'napplet' }; }
53
+ }
54
+
55
+ export async function lookup(prefixes, { relay = 'wss://wheat.oslim.dev', timeoutMs = 8000, nappletRelay = null } = {}) {
56
+ const filters = buildFilters(prefixes);
57
+ if (!filters.length) return { events: [], notices: [], error: 'no prefixes', via: 'none' };
58
+ if (nappletRelay) return viaNapplet(nappletRelay, relay, filters, timeoutMs);
59
+ if (typeof WebSocket === 'undefined') return { events: [], notices: [], error: 'no WebSocket here', via: 'none' };
60
+ return viaWebSocket(relay, filters, timeoutMs);
61
+ }
62
+
63
+ // which candidate prefix an event answers to, and how
64
+ export function matchPrefix(ev, prefixes) {
65
+ for (const p of prefixes) { const q = p.toLowerCase(); if (ev.id.startsWith(q)) return { prefix: q, how: 'event id' }; if (ev.pubkey.startsWith(q)) return { prefix: q, how: 'pubkey' }; }
66
+ return null;
67
+ }
68
+
69
+
70
+ // fetch kind 0 for full pubkeys (the authors of id-matched events)
71
+ export async function fetchProfiles(pubkeys, { relay = 'wss://wheat.oslim.dev', timeoutMs = 6000, nappletRelay = null } = {}) {
72
+ const pks = Array.from(new Set(pubkeys)).filter(p => /^[0-9a-f]{64}$/.test(p));
73
+ if (!pks.length) return {};
74
+ const filters = [{ authors: pks, kinds: [0] }];
75
+ const r = nappletRelay ? await viaNapplet(nappletRelay, relay, filters, timeoutMs) : await viaWebSocket(relay, filters, timeoutMs);
76
+ const out = {};
77
+ for (const ev of r.events) if (ev.kind === 0 && (!out[ev.pubkey] || out[ev.pubkey].created_at < ev.created_at)) out[ev.pubkey] = ev;
78
+ return out;
79
+ }
80
+
81
+ // does this relay honor partial ids and partial authors? Fetch one real event, then ask for it
82
+ // by an 8-character prefix of its id and of its pubkey.
83
+ export async function probeRelay(url, { timeoutMs = 6000, nappletRelay = null } = {}) {
84
+ const q = f => nappletRelay ? viaNapplet(nappletRelay, url, f, timeoutMs) : viaWebSocket(url, f, timeoutMs);
85
+ const a = await q([{ kinds: [0, 1], limit: 1 }]);
86
+ if (a.error && !a.events.length) return { ok: false, ids: 'unknown', authors: 'unknown', detail: a.error };
87
+ if (!a.events.length) return { ok: false, ids: 'unknown', authors: 'unknown', detail: 'relay returned no events to test with' };
88
+ const ev = a.events[0];
89
+ const verdict = r => (r.notices.some(n => /CLOSED|error|invalid|too small|bad req/i.test(n)) ? 'rejected' : r.events.some(e => e.id === ev.id) ? 'yes' : 'ignored');
90
+ const byId = await q([{ ids: [ev.id.slice(0, 8)] }]);
91
+ const byAuthor = await q([{ authors: [ev.pubkey.slice(0, 8)], kinds: [ev.kind], limit: 5 }]);
92
+ const ids = verdict(byId), authors = byAuthor.notices.some(n => /CLOSED|error|invalid|too small|bad req/i.test(n)) ? 'rejected' : byAuthor.events.some(e => e.pubkey === ev.pubkey) ? 'yes' : 'ignored';
93
+ const detail = [...byId.notices, ...byAuthor.notices].filter(Boolean)[0] || '';
94
+ return { ok: ids === 'yes' && authors === 'yes', ids, authors, detail };
95
+ }
package/src/nip19.js ADDED
@@ -0,0 +1,59 @@
1
+ // NIP-19 bech32 encoding: npub, note, nevent, naddr (with relay hints). Encode only.
2
+ const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
3
+ function polymod(values) { const G = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; let chk = 1;
4
+ for (const v of values) { const b = chk >> 25; chk = ((chk & 0x1ffffff) << 5) ^ v; for (let i = 0; i < 5; i++) if ((b >> i) & 1) chk ^= G[i]; } return chk; }
5
+ function hrpExpand(hrp) { const out = []; for (const c of hrp) out.push(c.charCodeAt(0) >> 5); out.push(0); for (const c of hrp) out.push(c.charCodeAt(0) & 31); return out; }
6
+ function toWords(bytes) { const out = []; let acc = 0, bits = 0; for (const b of bytes) { acc = (acc << 8) | b; bits += 8; while (bits >= 5) { bits -= 5; out.push((acc >> bits) & 31); } } if (bits > 0) out.push((acc << (5 - bits)) & 31); return out; }
7
+ export function bech32Encode(hrp, bytes) {
8
+ const words = toWords(bytes), pm = polymod(hrpExpand(hrp).concat(words, [0, 0, 0, 0, 0, 0])) ^ 1, chk = [];
9
+ for (let i = 0; i < 6; i++) chk.push((pm >> (5 * (5 - i))) & 31);
10
+ return hrp + '1' + words.concat(chk).map(w => CHARSET[w]).join('');
11
+ }
12
+ const hexBytes = h => Uint8Array.from(h.match(/../g).map(x => parseInt(x, 16)));
13
+ const utf8 = s => new TextEncoder().encode(s);
14
+ function tlv(items) { const parts = []; for (const [t, v] of items) { parts.push(t, v.length); for (const b of v) parts.push(b); } return Uint8Array.from(parts); }
15
+ export const npub = pk => bech32Encode('npub', hexBytes(pk));
16
+ export const note = id => bech32Encode('note', hexBytes(id));
17
+ export function nevent(id, { relays = [], author = null, kind = null } = {}) {
18
+ const items = [[0, hexBytes(id)]]; for (const r of relays) items.push([1, utf8(r)]);
19
+ if (author) items.push([2, hexBytes(author)]);
20
+ if (kind !== null) items.push([3, Uint8Array.from([(kind >>> 24) & 255, (kind >>> 16) & 255, (kind >>> 8) & 255, kind & 255])]);
21
+ return bech32Encode('nevent', tlv(items));
22
+ }
23
+ export function naddr(kind, pubkey, d, { relays = [] } = {}) {
24
+ const items = [[0, utf8(d)]]; for (const r of relays) items.push([1, utf8(r)]);
25
+ items.push([2, hexBytes(pubkey)], [3, Uint8Array.from([(kind >>> 24) & 255, (kind >>> 16) & 255, (kind >>> 8) & 255, kind & 255])]);
26
+ return bech32Encode('naddr', tlv(items));
27
+ }
28
+ export const isAddressable = kind => kind >= 30000 && kind < 40000;
29
+
30
+ // ---------------------------------------------------------------- decoding
31
+ function fromWords(words) { const out = []; let acc = 0, bits = 0; for (const w of words) { acc = (acc << 5) | w; bits += 5; while (bits >= 8) { bits -= 8; out.push((acc >> bits) & 255); } } return Uint8Array.from(out); }
32
+ export function bech32Decode(str) {
33
+ const s = str.toLowerCase(), pos = s.lastIndexOf('1');
34
+ if (pos < 1 || pos + 7 > s.length) throw new Error('not bech32');
35
+ const hrp = s.slice(0, pos), data = [];
36
+ for (const c of s.slice(pos + 1)) { const v = CHARSET.indexOf(c); if (v < 0) throw new Error('bad character'); data.push(v); }
37
+ if (polymod(hrpExpand(hrp).concat(data)) !== 1) throw new Error('bad checksum');
38
+ return { hrp, bytes: fromWords(data.slice(0, -6)) };
39
+ }
40
+ const hex = b => Array.from(b, x => x.toString(16).padStart(2, '0')).join('');
41
+ function parseTLV(bytes) { const out = []; let i = 0; while (i + 1 < bytes.length) { const t = bytes[i], l = bytes[i + 1], v = bytes.subarray(i + 2, i + 2 + l); if (v.length < l) break; out.push([t, v]); i += 2 + l; } return out; }
42
+ // Returns {type, hex, relays, kind, d, author} for npub, note, nprofile, nevent, naddr; throws otherwise.
43
+ export function decodeEntity(str) {
44
+ const { hrp, bytes } = bech32Decode(str.trim().replace(/^nostr:/i, ''));
45
+ if (hrp === 'npub' || hrp === 'note') { if (bytes.length !== 32) throw new Error('wrong length'); return { type: hrp, hex: hex(bytes), relays: [] }; }
46
+ if (hrp === 'nprofile' || hrp === 'nevent' || hrp === 'naddr') {
47
+ const out = { type: hrp, hex: null, relays: [], kind: null, d: null, author: null };
48
+ for (const [t, v] of parseTLV(bytes)) {
49
+ if (t === 0) out.hex = hrp === 'naddr' ? null : hex(v), out.d = hrp === 'naddr' ? new TextDecoder().decode(v) : null;
50
+ else if (t === 1) out.relays.push(new TextDecoder().decode(v));
51
+ else if (t === 2) out.author = hex(v);
52
+ else if (t === 3 && v.length === 4) out.kind = (v[0] << 24 | v[1] << 16 | v[2] << 8 | v[3]) >>> 0;
53
+ }
54
+ if (hrp === 'naddr') out.hex = out.author; // no event id in an naddr: the author's pubkey is the best prefix we have
55
+ if (!out.hex) throw new Error('missing data');
56
+ return out;
57
+ }
58
+ throw new Error('unsupported: ' + hrp);
59
+ }
package/src/nn.js ADDED
@@ -0,0 +1,81 @@
1
+ // Plain-JavaScript inference for the glyphbyte classifier. Weights are a flat float16 blob
2
+ // (batch-norm already folded into the convolutions) described by a manifest.
3
+
4
+ export function f16ToF32(u16) {
5
+ const out = new Float32Array(u16.length);
6
+ for (let i = 0; i < u16.length; i++) {
7
+ const h = u16[i], s = (h & 0x8000) ? -1 : 1, e = (h >> 10) & 0x1f, m = h & 0x3ff;
8
+ if (e === 0) out[i] = s * m * Math.pow(2, -24);
9
+ else if (e === 31) out[i] = m ? NaN : s * Infinity;
10
+ else out[i] = s * (1 + m / 1024) * Math.pow(2, e - 15);
11
+ }
12
+ return out;
13
+ }
14
+
15
+ export function loadWeights(manifest, bytes) {
16
+ const u16 = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 1);
17
+ const all = f16ToF32(u16);
18
+ const w = {};
19
+ for (const l of manifest.layers) w[l.name] = { data: all.subarray(l.offset, l.offset + l.size), shape: l.shape };
20
+ return { manifest, w };
21
+ }
22
+
23
+ // conv 3x3, padding 1, followed by relu. inp is C*H*W planar, w is O*C*3*3, b is O.
24
+ // The input is copied once into a zero-padded buffer so the inner loop has no bounds checks.
25
+ function conv3x3Relu(inp, C, H, W, w, b, O) {
26
+ const PW = W + 2, PH = H + 2, pad = new Float32Array(C * PW * PH);
27
+ for (let c = 0; c < C; c++) for (let y = 0; y < H; y++) pad.set(inp.subarray(c * H * W + y * W, c * H * W + (y + 1) * W), c * PW * PH + (y + 1) * PW + 1);
28
+ const out = new Float32Array(O * H * W), HW = H * W;
29
+ for (let o = 0; o < O; o++) {
30
+ const ob = o * HW;
31
+ out.fill(b[o], ob, ob + HW);
32
+ for (let c = 0; c < C; c++) {
33
+ const wb = (o * C + c) * 9, pb = c * PW * PH;
34
+ const w0 = w[wb], w1 = w[wb + 1], w2 = w[wb + 2], w3 = w[wb + 3], w4 = w[wb + 4], w5 = w[wb + 5], w6 = w[wb + 6], w7 = w[wb + 7], w8 = w[wb + 8];
35
+ for (let y = 0; y < H; y++) {
36
+ let r0 = pb + y * PW, r1 = r0 + PW, r2 = r1 + PW, oi = ob + y * W;
37
+ for (let x = 0; x < W; x++, r0++, r1++, r2++, oi++) {
38
+ out[oi] += pad[r0] * w0 + pad[r0 + 1] * w1 + pad[r0 + 2] * w2 + pad[r1] * w3 + pad[r1 + 1] * w4 + pad[r1 + 2] * w5 + pad[r2] * w6 + pad[r2 + 1] * w7 + pad[r2 + 2] * w8;
39
+ }
40
+ }
41
+ }
42
+ for (let i = ob; i < ob + HW; i++) if (out[i] < 0) out[i] = 0;
43
+ }
44
+ return out;
45
+ }
46
+
47
+ function maxPool2(inp, C, H, W) {
48
+ const h = H >> 1, w = W >> 1, out = new Float32Array(C * h * w);
49
+ for (let c = 0; c < C; c++) for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
50
+ const i = c * H * W + 2 * y * W + 2 * x;
51
+ out[c * h * w + y * w + x] = Math.max(inp[i], inp[i + 1], inp[i + W], inp[i + W + 1]);
52
+ }
53
+ return out;
54
+ }
55
+
56
+ function softmax(v) {
57
+ let m = -Infinity; for (const x of v) if (x > m) m = x;
58
+ const e = v.map(x => Math.exp(x - m)); const s = e.reduce((a, b) => a + b, 0);
59
+ return e.map(x => x / s);
60
+ }
61
+
62
+ // patch: Float32Array of PATCH*PATCH values in 0..1. Returns {symRot: 64 probs, junk, fill: 2 probs}
63
+ export function classify(model, patch, size) {
64
+ const { w, manifest } = model;
65
+ const ch = manifest.channels;
66
+ let x = patch, C = 1, H = size, W = size;
67
+ for (let bi = 0; bi < 4; bi++) {
68
+ const O = ch[bi];
69
+ x = conv3x3Relu(x, C, H, W, w[`conv${bi}_0_w`].data, w[`conv${bi}_0_b`].data, O); C = O;
70
+ x = conv3x3Relu(x, C, H, W, w[`conv${bi}_1_w`].data, w[`conv${bi}_1_b`].data, O);
71
+ x = maxPool2(x, C, H, W); H >>= 1; W >>= 1;
72
+ }
73
+ const feat = new Float32Array(C);
74
+ for (let c = 0; c < C; c++) { let s = 0; for (let i = 0; i < H * W; i++) s += x[c * H * W + i]; feat[c] = s / (H * W); }
75
+ const dense = (name) => { const W_ = w[name + "_w"], b = w[name + "_b"].data, n = W_.shape[0], out = new Array(n);
76
+ for (let o = 0; o < n; o++) { let s = b[o]; for (let c = 0; c < C; c++) s += W_.data[o * C + c] * feat[c]; out[o] = s; } return out; };
77
+ const sym = softmax(dense("sym")), fill = softmax(dense("fill"));
78
+ const junk = sym.length > 64 ? sym[64] : 0;
79
+ const real = sym.slice(0, 64); const rs = real.reduce((a, b) => a + b, 0) || 1;
80
+ return { symRot: real.map(p => p / rs), junk, fill };
81
+ }
@@ -0,0 +1,42 @@
1
+ // Image to candidate byte sequences: port of glyphbyte/pipeline.py.
2
+ import { detect } from './detect.js';
3
+ import { classify } from './nn.js';
4
+
5
+ export const SYMBOLS = ['house', 'chevron', 'bookmark', 'crown', 'drop', 'tee', 'u', 'mountain', 'arrow', 'heart', 'crescent', 'cloud', 'snowman', 'l', 'trapezoid', 'pacman'];
6
+ export function unpack(b) { return { symbol: b >> 4, rotation: (b >> 2) & 3, fill: (b >> 1) & 1, frame: b & 1, name: SYMBOLS[b >> 4] }; }
7
+ export function describe(b) { const g = unpack(b); return `${g.name} rotated ${g.rotation * 90} deg, ${g.fill ? 'filled' : 'outline'}, ${g.frame ? 'circle' : 'square'} frame`; }
8
+
9
+ function byteDistribution(sc, kind, conf) {
10
+ const pf = [0.5, 0.5]; pf[kind] = 0.5 + 0.5 * conf; pf[1 - kind] = 0.5 - 0.5 * conf;
11
+ const dist = new Float64Array(256); let s = 0;
12
+ for (let sr = 0; sr < 64; sr++) for (let fill = 0; fill < 2; fill++) for (let fr = 0; fr < 2; fr++) { const v = sc.symRot[sr] * sc.fill[fill] * pf[fr]; dist[(sr << 2) | (fill << 1) | fr] = v; s += v; }
13
+ for (let i = 0; i < 256; i++) dist[i] /= s || 1;
14
+ return dist;
15
+ }
16
+ function candidates(dist, forkRatio, maxPer) {
17
+ const order = Array.from(dist.keys()).sort((a, b) => dist[b] - dist[a]), top = dist[order[0]], out = [[order[0], top]];
18
+ for (let i = 1; i < maxPer; i++) if (dist[order[i]] >= forkRatio * top) out.push([order[i], dist[order[i]]]);
19
+ return out;
20
+ }
21
+ function sequences(reads, maxSeq) {
22
+ let beam = [[[], 1]];
23
+ for (const r of reads) { const nxt = []; for (const [seq, p] of beam) for (const [b, pb] of r.candidates) nxt.push([seq.concat([b]), p * pb]); nxt.sort((a, b) => b[1] - a[1]); beam = nxt.slice(0, maxSeq); }
24
+ const tot = beam.reduce((s, x) => s + x[1], 0) || 1;
25
+ return beam.map(([seq, p]) => ({ bytes: seq, hex: seq.map(b => b.toString(16).padStart(2, '0')).join(''), p: p / tot }));
26
+ }
27
+
28
+ export function readImage(gray, W, H, model, opts = {}) {
29
+ const maxSeq = opts.maxSequences || 8, forkRatio = opts.forkRatio || 0.2, maxPer = opts.maxPerSymbol || 4;
30
+ const junkFn = patches => patches.map(p => classify(model, p, 64).junk);
31
+ const det = detect(gray, W, H, junkFn);
32
+ const warnings = det.warnings.slice();
33
+ if (!det.frames.length) return { reads: [], sequences: [], warnings: warnings.concat(['no symbols found']), detection: det };
34
+ const reads = det.frames.map((f, i) => { const sc = classify(model, det.patches[i], 64); const c = candidates(byteDistribution(sc, f.kind, f.conf), forkRatio, maxPer); return { index: i, candidates: c, byte: c[0][0], p: c[0][1], frameConf: f.conf }; });
35
+ let seqs = sequences(reads, maxSeq);
36
+ if (det.baseline && !det.startKnown) {
37
+ const rev = seqs.map(s => ({ bytes: s.bytes.slice().reverse(), hex: s.bytes.slice().reverse().map(b => b.toString(16).padStart(2, '0')).join(''), p: s.p * 0.5 }));
38
+ seqs = seqs.map(s => ({ ...s, p: s.p * 0.5 })).concat(rev).sort((a, b) => b.p - a.p).slice(0, maxSeq);
39
+ warnings.push('reading direction unknown: sequences include the reversed order');
40
+ }
41
+ return { reads, sequences: seqs, best: seqs[0].hex, warnings, detection: det };
42
+ }
package/src/render.js ADDED
@@ -0,0 +1,61 @@
1
+ // Canvas rendering: bytes to symbols to copy from, the reference sheet, and the detection overlay.
2
+ // SHAPES is the canonical list: [{name, outer:[[x,y]...], features:[[[x,y]...]]}] in a unit box.
3
+
4
+ function glyphPath(ctx, shape, rot, cx, cy, size) {
5
+ const t = rot * Math.PI / 2, c = Math.cos(t), s = Math.sin(t);
6
+ const put = (poly) => { poly.forEach((p, i) => { const x = cx + (p[0] * c - p[1] * s) * size, y = cy + (p[0] * s + p[1] * c) * size; i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }); ctx.closePath(); };
7
+ ctx.beginPath(); put(shape.outer); for (const f of shape.features) put(f);
8
+ }
9
+
10
+ export function drawCell(ctx, SHAPES, byte, cx, cy, cell, lw) {
11
+ const g = { symbol: byte >> 4, rotation: (byte >> 2) & 3, fill: (byte >> 1) & 1, frame: byte & 1 };
12
+ ctx.lineWidth = lw; ctx.strokeStyle = '#000'; ctx.fillStyle = '#000'; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
13
+ ctx.beginPath();
14
+ if (g.frame) ctx.arc(cx, cy, cell / 2, 0, 2 * Math.PI); else ctx.rect(cx - cell / 2, cy - cell / 2, cell, cell);
15
+ ctx.stroke();
16
+ const size = cell * (g.frame ? 0.56 : 0.62);
17
+ glyphPath(ctx, SHAPES[g.symbol], g.rotation, cx, cy, size);
18
+ if (g.fill) ctx.fill('evenodd');
19
+ ctx.stroke();
20
+ }
21
+
22
+ // returns the canvas size used; bytes is an array of 0..255
23
+ export function drawRow(canvas, SHAPES, bytes, cell = 120, baseline = true) {
24
+ const margin = Math.round(cell * 0.6), pitch = cell * 1.35, n = bytes.length;
25
+ const W = Math.round(margin * 2 + pitch * n), H = Math.round(margin * 2 + cell * 1.5);
26
+ canvas.width = W; canvas.height = H;
27
+ const ctx = canvas.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H);
28
+ const lw = Math.max(2, cell * 0.045), y = margin + cell * 0.55;
29
+ bytes.forEach((b, i) => drawCell(ctx, SHAPES, b, margin + cell * 0.6 + pitch * i, y, cell, lw));
30
+ if (baseline && n) {
31
+ const y0 = y + cell * 0.72, x0 = margin * 0.5, x1 = W - margin * 0.5;
32
+ ctx.lineWidth = lw; ctx.strokeStyle = '#000'; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y0); ctx.stroke();
33
+ ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(x0, y0, Math.max(3, cell * 0.13), 0, 2 * Math.PI); ctx.fill();
34
+ }
35
+ return { W, H };
36
+ }
37
+
38
+ export function drawSheet(canvas, SHAPES, cell = 64) {
39
+ const rows = 16, cols = 8, pitch = Math.round(cell * 1.25);
40
+ canvas.width = pitch * cols + cell; canvas.height = pitch * rows + cell;
41
+ const ctx = canvas.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
42
+ for (let s = 0; s < 16; s++) for (let c = 0; c < cols; c++) {
43
+ const fill = c >> 2, rot = c & 3, frame = (s + c) & 1, b = (s << 4) | (rot << 2) | (fill << 1) | frame;
44
+ drawCell(ctx, SHAPES, b, cell * 0.6 + pitch * c, cell * 0.6 + pitch * s, cell, Math.max(2, cell * 0.045));
45
+ }
46
+ }
47
+
48
+ // draw what the detector found on top of the (downscaled) photo already painted on the canvas
49
+ export function drawDetection(ctx, det) {
50
+ ctx.lineWidth = 2; ctx.font = '14px sans-serif';
51
+ det.frames.forEach((f, i) => {
52
+ ctx.strokeStyle = f.kind === 0 ? '#00c853' : '#ff9100'; ctx.beginPath();
53
+ f.hull.forEach((p, k) => k ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); ctx.closePath(); ctx.stroke();
54
+ ctx.fillStyle = '#ff1744'; ctx.fillText(String(i), f.center[0] - 4, f.center[1] + 5);
55
+ });
56
+ if (det.baseline) {
57
+ const [a, b] = det.baseline; ctx.strokeStyle = '#d500f9'; ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
58
+ ctx.beginPath(); ctx.arc(a[0], a[1], 8, 0, 2 * Math.PI); ctx.lineWidth = det.startKnown ? 3 : 1; ctx.stroke();
59
+ const mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2; ctx.strokeStyle = '#ffea00'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(mx, my); ctx.lineTo(mx + det.up[0] * 40, my + det.up[1] * 40); ctx.stroke();
60
+ }
61
+ }
package/weights.bin ADDED
Binary file
@@ -0,0 +1 @@
1
+ {"channels": [16, 32, 64, 96], "layers": [{"name": "conv0_0_w", "shape": [16, 1, 3, 3], "offset": 0, "size": 144}, {"name": "conv0_0_b", "shape": [16], "offset": 144, "size": 16}, {"name": "conv0_1_w", "shape": [16, 16, 3, 3], "offset": 160, "size": 2304}, {"name": "conv0_1_b", "shape": [16], "offset": 2464, "size": 16}, {"name": "conv1_0_w", "shape": [32, 16, 3, 3], "offset": 2480, "size": 4608}, {"name": "conv1_0_b", "shape": [32], "offset": 7088, "size": 32}, {"name": "conv1_1_w", "shape": [32, 32, 3, 3], "offset": 7120, "size": 9216}, {"name": "conv1_1_b", "shape": [32], "offset": 16336, "size": 32}, {"name": "conv2_0_w", "shape": [64, 32, 3, 3], "offset": 16368, "size": 18432}, {"name": "conv2_0_b", "shape": [64], "offset": 34800, "size": 64}, {"name": "conv2_1_w", "shape": [64, 64, 3, 3], "offset": 34864, "size": 36864}, {"name": "conv2_1_b", "shape": [64], "offset": 71728, "size": 64}, {"name": "conv3_0_w", "shape": [96, 64, 3, 3], "offset": 71792, "size": 55296}, {"name": "conv3_0_b", "shape": [96], "offset": 127088, "size": 96}, {"name": "conv3_1_w", "shape": [96, 96, 3, 3], "offset": 127184, "size": 82944}, {"name": "conv3_1_b", "shape": [96], "offset": 210128, "size": 96}, {"name": "sym_w", "shape": [65, 96], "offset": 210224, "size": 6240}, {"name": "sym_b", "shape": [65], "offset": 216464, "size": 65}, {"name": "fill_w", "shape": [2, 96], "offset": 216529, "size": 192}, {"name": "fill_b", "shape": [2], "offset": 216721, "size": 2}], "n_classes": 65, "patch": 64}