├── .gitignore ├── README.md ├── Writing.qml ├── asset ├── bg.png └── screenshot.png ├── handwriting.pro ├── handwritingengine.cpp ├── handwritingengine.h ├── js └── shortstraw.js ├── main.cpp └── main.qml /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.moc 3 | Makefile 4 | qml-handwriting 5 | Canvas/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QML Handwriting 2 | 3 | ![qml-handwriting](https://github.com/penk/qml-handwriting/raw/master/asset/screenshot.png) 4 | 5 | ### Open source handwriting recognition keyboard based on Qt/QML 6 | 7 | Built out of QML, ShortStrawJS and Zinnia. 8 | 9 | ## Features 10 | 11 | * Pure QML-based layout, can integrate with input method panel 12 | * Supports Chinese (Traditional/Simplified) and Japanese, models and engines from Zinnia 13 | * Strokes detection and recognition based on ShortStraw algorithm 14 | 15 | ## How to Install 16 | 17 | 1. Install [Zinnia](http://zinnia.sourceforge.net/) library 18 | 2. Install QML [Canvas](http://qt.gitorious.org/qt-labs/qmlcanvas) plugin 19 | 3. Check-out source code and compile: `git clone https://github.com/penk/qml-handwriting.git` 20 | 4. Download [handwriting models](http://www.tegaki.org/releases/0.3/models/), default path is `/usr/share/tegaki/models/zinnia/handwriting-zh_TW.model` 21 | 22 | ## License 23 | 24 | The source codes are, unless otherwise specified, distributed under the terms of the GNU Lesser General Public License. 25 | 26 | ## Credits 27 | 28 | Copyright (C) 2012 Ping-Hsun Chen <[penkia@gmail.com](mailto:penkia@gmail.com)>, [@penk](https://twitter.com/penk) 29 | 30 | Includes: 31 | 32 | * [ShortStrawJS](http://www.lab4games.net/zz85/blog/2010/01/21/geeknotes-shortstrawjs-fast-and-simple-corner-detection/) by [Joshua Koo](mailto:zz85nus@gmail.com) 33 | * [qmlcanvas](http://qt.gitorious.org/qt-labs/qmlcanvas/) by Qt Labs 34 | * Zinnia models from [Tegaki](http://tegaki.org) project 35 | -------------------------------------------------------------------------------- /Writing.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | import "js/shortstraw.js" as Straw 4 | 5 | Canvas { 6 | id:canvas 7 | 8 | property int paintX 9 | property int paintY 10 | property int count: 0 11 | property int strokes: 0 12 | property int lineWidth: 5 13 | property string drawColor: "black" 14 | property variant myArray: [] 15 | 16 | // required to work (handwritingEngine) 17 | property QtObject engine 18 | 19 | function addItem(x, y) { 20 | // Properties in QML are not real JavaScript arrays, 21 | // but sort of pretend to be. 22 | // They cannot be mutated. 23 | // Workaround : 24 | var tmp = myArray; 25 | tmp.push({'x':x, 'y':y}) 26 | myArray = tmp; 27 | } 28 | 29 | MouseArea { 30 | id:mousearea 31 | hoverEnabled:true 32 | anchors.fill: parent 33 | onClicked: drawPoint(); 34 | 35 | onPositionChanged: { 36 | requestPaint() 37 | } 38 | 39 | onPressed: { 40 | paintX = mouseX; 41 | paintY = mouseY; 42 | } 43 | 44 | onReleased: { 45 | var array = Straw.shortStraw(myArray); 46 | var ctx = getContext("2d") 47 | 48 | ctx.beginPath(); 49 | ctx.strokeStyle = 'red'; 50 | ctx.moveTo(array[0].x, array[0].y); 51 | ctx.lineWidth = 2; 52 | for (var i = 0; i < array.length; i++) { 53 | // console.log("strokes "+strokes+": " + array[i].x + ", "+ array[i].y ); 54 | engine.query(strokes, array[i].x, array[i].y); 55 | if (i > 0) 56 | ctx.lineTo(array[i].x, array[i].y); 57 | } 58 | 59 | ctx.stroke(); 60 | ctx.closePath(); 61 | 62 | myArray = []; 63 | strokes++; 64 | } 65 | } 66 | 67 | onPaint: { 68 | // draw segments 69 | if (mousearea.pressed) { 70 | var ctx = getContext("2d") 71 | ctx.strokeStyle = drawColor 72 | ctx.lineWidth = lineWidth 73 | ctx.beginPath() 74 | ctx.moveTo(paintX, paintY) 75 | paintX = mousearea.mouseX; 76 | paintY = mousearea.mouseY; 77 | ctx.lineTo(mousearea.mouseX, mousearea.mouseY) 78 | ctx.stroke() 79 | ctx.closePath() 80 | addItem(paintX, paintY) 81 | } 82 | } 83 | 84 | function drawPoint() { 85 | var ctx = canvas.getContext("2d") 86 | 87 | ctx.lineWidth = lineWidth 88 | ctx.fillStyle = drawColor 89 | ctx.fillRect(mousearea.mouseX, mousearea.mouseY, 2, 2); 90 | } 91 | 92 | // reset everything 93 | function clear() { 94 | var ctx = canvas.getContext("2d") 95 | 96 | strokes = 0; 97 | engine.clear(); 98 | ctx.clearRect(0, 0, width, height); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /asset/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penk/qml-handwriting/df6961eeabf8736f548211c58296cf074c232980/asset/bg.png -------------------------------------------------------------------------------- /asset/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penk/qml-handwriting/df6961eeabf8736f548211c58296cf074c232980/asset/screenshot.png -------------------------------------------------------------------------------- /handwriting.pro: -------------------------------------------------------------------------------- 1 | QT += gui quick qml 2 | 3 | TEMPLATE = app 4 | TARGET = qml-handwriting 5 | CONFIG += qt 6 | 7 | 8 | unix { 9 | # CONFIG += link_pkgconfig 10 | # PKGCONFIG += zinnia 11 | LIBS += -L/usr/local/lib/ -lzinnia 12 | } 13 | 14 | HEADERS += \ 15 | handwritingengine.h \ 16 | 17 | SOURCES += \ 18 | handwritingengine.cpp \ 19 | main.cpp 20 | 21 | OTHER_FILES += *.qml 22 | -------------------------------------------------------------------------------- /handwritingengine.cpp: -------------------------------------------------------------------------------- 1 | #include "handwritingengine.h" 2 | #include 3 | #include 4 | 5 | HandwritingEngine::HandwritingEngine(QObject *parent) : 6 | QObject(parent) 7 | , m_results_size(8) 8 | , m_drawing_height(263) 9 | , m_drawing_width(369) 10 | , m_model_path("/usr/share/tegaki/models/zinnia/handwriting-zh_TW.model") 11 | , m_loaded(false) 12 | { 13 | m_recognizer = zinnia::Recognizer::create(); 14 | loadModel(m_model_path); 15 | 16 | m_character = zinnia::Character::create(); 17 | m_character->clear(); 18 | 19 | m_character->set_height(m_drawing_height); 20 | m_character->set_width(m_drawing_width); 21 | } 22 | 23 | // Load the language model for text recognition 24 | bool HandwritingEngine::loadModel(const QString model) 25 | { 26 | if (!m_recognizer->open(model.toLocal8Bit())) { 27 | qDebug("can't load model file"); 28 | m_loaded = false; 29 | return m_loaded; 30 | } 31 | qDebug() << "model" << model << " loaded"; 32 | m_loaded = true; 33 | return m_loaded; 34 | } 35 | 36 | // Process the drawn lines to produces results from the model 37 | QStringList HandwritingEngine::query(int s, int x, int y) 38 | { 39 | m_results_list.clear(); 40 | emit resultsChanged(m_results_list); 41 | 42 | // add a line to the current character 43 | m_character->add(s, x, y); 44 | 45 | // based on current character being drawn, we get some result 46 | m_result = m_recognizer->classify(*m_character, m_results_size); 47 | if (!m_result) 48 | qDebug("can't find m_result"); 49 | else 50 | { 51 | for (u_int32_t i = 0; i < m_result->size(); ++i) 52 | { 53 | // qDebug() << "m_result found : " << QString::fromLocal8Bit(m_result->value(i)); 54 | m_results_list << QString::fromLocal8Bit(m_result->value(i)); 55 | emit resultsChanged(m_results_list); 56 | } 57 | } 58 | return m_results_list; 59 | } 60 | 61 | void HandwritingEngine::clear() 62 | { 63 | m_character->clear(); 64 | m_results_list.clear(); 65 | emit resultsChanged(m_results_list); 66 | } 67 | 68 | QStringList HandwritingEngine::results() const 69 | { 70 | return m_results_list; 71 | } 72 | 73 | int HandwritingEngine::drawing_height() const 74 | { 75 | return m_drawing_height; 76 | } 77 | 78 | int HandwritingEngine::drawing_width() const 79 | { 80 | return m_drawing_width; 81 | } 82 | 83 | bool HandwritingEngine::loaded() const 84 | { 85 | return m_loaded; 86 | } 87 | 88 | void HandwritingEngine::setDrawing_height(const int height) 89 | { 90 | m_drawing_height = height; 91 | m_character->set_height(m_drawing_height); 92 | emit drawing_heightChanged(m_drawing_height); 93 | } 94 | 95 | void HandwritingEngine::setDrawing_width(const int width) 96 | { 97 | m_drawing_width = width; 98 | m_character->set_width(m_drawing_width); 99 | emit drawing_widthChanged(m_drawing_width); 100 | } 101 | -------------------------------------------------------------------------------- /handwritingengine.h: -------------------------------------------------------------------------------- 1 | #ifndef HandwritingEngine_H 2 | #define HandwritingEngine_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class HandwritingEngine : public QObject 9 | { 10 | Q_OBJECT 11 | 12 | Q_PROPERTY(QStringList results READ results NOTIFY resultsChanged) 13 | Q_PROPERTY(int drawing_height READ drawing_height WRITE setDrawing_height NOTIFY drawing_heightChanged) 14 | Q_PROPERTY(int drawing_width READ drawing_width WRITE setDrawing_width NOTIFY drawing_widthChanged) 15 | Q_PROPERTY(bool loaded READ loaded NOTIFY loadedChanged) 16 | public: 17 | explicit HandwritingEngine(QObject *parent = 0); 18 | 19 | bool loadModel(const QString model); 20 | Q_INVOKABLE QStringList query(int s, int x, int y); 21 | Q_INVOKABLE void clear(); 22 | 23 | QStringList results() const; 24 | int drawing_height() const; 25 | int drawing_width() const; 26 | bool loaded() const; 27 | 28 | signals: 29 | QStringList resultsChanged(QStringList); 30 | int drawing_heightChanged(int); 31 | int drawing_widthChanged(int); 32 | bool loadedChanged(bool); 33 | 34 | public slots: 35 | void setDrawing_height(const int height); 36 | void setDrawing_width(const int width); 37 | 38 | private: 39 | zinnia::Recognizer* m_recognizer; 40 | zinnia::Character* m_character; 41 | zinnia::Result* m_result; 42 | 43 | QStringList m_results_list; 44 | int m_results_size; 45 | int m_drawing_height; 46 | int m_drawing_width; 47 | QString m_model_path; 48 | bool m_loaded; 49 | }; 50 | 51 | #endif // HandwritingEngine_H 52 | -------------------------------------------------------------------------------- /js/shortstraw.js: -------------------------------------------------------------------------------- 1 | // ShortStrawJS, a javascript implementation 2 | // http://www.lab4games.net/zz85/blog/2010/01/21/geeknotes-shortstrawjs-fast-and-simple-corner-detection/ 3 | // 4 | // Derived heavily from the AS3 implementation of the ShortStraw Corner Finder (Wolin et al. 2008) 5 | // by Felix Raab. 21 July 2009. 6 | // http://www.betriebsraum.de/blog/2009/07/21/efficient-gesture-recognition-and-corner-finding-in-as3/ 7 | // 8 | // Based on the paper ShortStraw: A Simple and Effective Corner Finder for Polylines 9 | // http://srlweb.cs.tamu.edu/srlng_media/content/objects/object-1246294647-350817e4b0870da27e16472ed36475db/Wolin_SBIM08.pdf 10 | // 11 | // For comments on this JS port, email Joshua Koo (zz85nus @ gmail.com) 12 | // 13 | // Released under MIT license: http://www.opensource.org/licenses/mit-license.php 14 | 15 | var shortStraw = function(points){ 16 | shortStraw.DIAGONAL_INTERVAL = 40; 17 | shortStraw.STRAW_WINDOW = 3; 18 | shortStraw.MEDIAN_THRESHOLD = 0.95; 19 | shortStraw.LINE_THRESHOLD = 0.95; 20 | 21 | // 1. get resample spacing. 22 | var s = shortStraw.determineResampleSpacing(points); 23 | 24 | // 2. get resample points. 25 | var resampled = shortStraw.resamplePoints(points, s); 26 | 27 | // 3. get corners 28 | var corners = shortStraw.getCorners(resampled); 29 | //debug(corners); 30 | 31 | var cornerPoints = []; 32 | for (var i in corners) { 33 | cornerPoints.push(resampled[corners[i]]); 34 | } 35 | // debug(cornerPoints); 36 | 37 | //4. return corners. 38 | return cornerPoints; 39 | }; 40 | 41 | 42 | shortStraw.determineResampleSpacing = function(points) { 43 | var b = shortStraw.boundingBox(points); 44 | var p1 = {x:b.x, y:b.y}; // topleft 45 | var p2 = {x:b.x + b.w, y:b.y + b.h};// bottomRight 46 | var d = shortStraw.distance(p1, p2); 47 | return d / shortStraw.DIAGONAL_INTERVAL; 48 | } 49 | 50 | shortStraw.resamplePoints = function(points,s) { 51 | var distance = 0; 52 | var resampled = []; 53 | resampled.push(points[0]); 54 | for (var i=1; i= s) { 60 | var qx = 61 | p1.x + ((s - distance) /d2) * 62 | (p2.x - p1.x); 63 | var qy = 64 | p1.y + ((s - distance) /d2) * 65 | (p2.y - p1.y); 66 | var q = {x:qx, y:qy}; 67 | resampled.push (q); 68 | points.splice(i, 0, q); 69 | distance= 0; 70 | } else { 71 | distance += d2; // Add path distance to total distance 72 | } 73 | } // End the loop and return resampled points 74 | return resampled; 75 | } 76 | 77 | shortStraw.getCorners = function (points) { 78 | 79 | var corners = [0]; 80 | var w = shortStraw.STRAW_WINDOW; 81 | var straws = []; 82 | var i; 83 | for (i=w; i c1 && newCorner < c2) { 126 | corners.splice(i,0,newCorner); 127 | go = false; 128 | } 129 | } 130 | } 131 | } 132 | 133 | for (i = 1; i < corners.length - 1; i++) { 134 | c1 = corners[i - 1]; 135 | c2 = corners[i + 1]; 136 | if (this.isLine(points, c1, c2)) { 137 | corners.splice(i, 1); 138 | i--; 139 | } 140 | } 141 | return corners; 142 | } 143 | 144 | shortStraw.halfwayCorner = function(straws,a,b) { 145 | var quarter = (b - a) / 4; 146 | var minValue = Number.POSITIVE_INFINITY; 147 | var minIndex; 148 | var w = shortStraw.STRAW_WINDOW; 149 | for (var i = a + quarter; i < (b - quarter); i++) { 150 | //var s = straws[i - w]; 151 | if (straws[i] < minValue) { 152 | minValue = straws[i]; 153 | minIndex = i; 154 | } 155 | } 156 | return minIndex; 157 | } 158 | 159 | shortStraw.boundingBox = function(points) { 160 | var minX = Number.POSITIVE_INFINITY; 161 | var maxX = Number.NEGATIVE_INFINITY; 162 | var minY = Number.POSITIVE_INFINITY; 163 | var maxY = Number.NEGATIVE_INFINITY; 164 | for (var i in points) { 165 | var p = points[i]; 166 | if (p.x < minX) { 167 | minX = p.x; 168 | } 169 | if (p.x > maxX) { 170 | maxX = p.x; 171 | } 172 | if (p.y < minY) { 173 | minY = p.y; 174 | } 175 | if (p.y > maxY) { 176 | maxY = p.y; 177 | } 178 | } 179 | return {x:minX, y:minY, w:maxX - minX,h:maxY - minY}; 180 | } 181 | 182 | shortStraw.distance = function (p1, p2) { 183 | var dx = p2.x - p1.x; 184 | var dy = p2.y - p1.y; 185 | return Math.pow((dx*dx + dy*dy), 1/2); 186 | } 187 | 188 | shortStraw.isLine = function(points, a, b) { 189 | var distance = shortStraw.distance(points[a], points[b]); 190 | var pathDistance = shortStraw.pathDistance(points, a, b); 191 | return (distance / pathDistance) > shortStraw.LINE_THRESHOLD; 192 | } 193 | 194 | 195 | shortStraw.pathDistance = function(points, a, b) { 196 | var d = 0; 197 | for (var i= a; i < b; i++) { 198 | d += shortStraw.distance(points[i], points[i + 1]); 199 | } 200 | return d; 201 | } 202 | 203 | 204 | shortStraw.median = function(values) { 205 | var s = values.concat(); 206 | s.sort(); 207 | var m; 208 | if (s.length % 2 == 0) { 209 | m = s.length / 2; 210 | return (s[m - 1] + s[m]) / 2; 211 | } else { 212 | m = (s.length + 1) / 2; 213 | return s[m - 1]; 214 | } 215 | } 216 | 217 | /* 218 | function debug(o) { 219 | alert(JSON.stringify(o)); 220 | }*/ 221 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "handwritingengine.h" 6 | 7 | 8 | int main(int argc, char *argv[]) { 9 | 10 | QGuiApplication app(argc, argv); 11 | QQuickView view; 12 | 13 | HandwritingEngine data; 14 | data.loadModel(QStringLiteral("/home/greys/Downloads/zinnia-tomoe-0.6.0-20080911/handwriting-zh_CN.model")); 15 | 16 | view.rootContext()->setContextProperty("Zinnia", &data); 17 | view.setSource(QUrl::fromLocalFile("main.qml")); 18 | view.show(); 19 | 20 | return app.exec(); 21 | } 22 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | 3 | Item { 4 | id:root 5 | width:768 6 | height:263 7 | anchors.margins: 4 8 | 9 | Image { source: "asset/bg.png"; anchors.fill: parent; } 10 | 11 | // Drawing zone for Text recognition 12 | Writing { 13 | id:canvas 14 | width: Zinnia.drawing_width; 15 | height: Zinnia.drawing_height; 16 | x: 200 17 | y: 0 18 | // set the handwritingEngine in the writing zone 19 | engine: Zinnia 20 | } 21 | 22 | // Presenting the results on the right 23 | Flow { 24 | anchors.top: root.top; 25 | anchors.left: canvas.right 26 | anchors.right: root.right 27 | 28 | Repeater { 29 | model: Zinnia.results 30 | delegate: Item { 31 | id: delegateItem 32 | width: 99 33 | height: delegateText.height 34 | Rectangle{ 35 | id: delegateBackground 36 | anchors.fill: parent; 37 | color: "transparent" 38 | opacity: .5 39 | } 40 | Text { 41 | id: delegateText 42 | width: parent.width 43 | font.pointSize: 42 44 | text: modelData 45 | horizontalAlignment: Text.AlignHCenter 46 | } 47 | MouseArea { 48 | anchors.fill: parent 49 | onPressed: delegateBackground.color = "black" 50 | onReleased: delegateBackground.color = "transparent" 51 | // Print the selected result 52 | onClicked: console.log(modelData) 53 | } 54 | } 55 | } 56 | } 57 | 58 | Item { 59 | id:clearbutton 60 | width:100 61 | height:65 62 | 63 | anchors.left: parent.left 64 | anchors.top: parent.top 65 | anchors.leftMargin: 100 66 | 67 | MouseArea { 68 | anchors.fill:parent 69 | onClicked: canvas.clear(); 70 | } 71 | } 72 | } 73 | --------------------------------------------------------------------------------