locked
Recursively Calling the recognizeAsync() Method

    Question

  • Hi all,

    I have a very interesting situation, and I was just wondering if anybody could help shed some light on what is going wrong. Basically, I have an application that uses Inking Libraries to allow users to write on digital forms. What I have is different event listeners on each of the fields that activate when the user inks them, and keep track of any inkStroke objects that were created on them.  I have 1 global InkManager that holds all of these objects, (it has no knowledge of the stroke classifications).

    What I am trying to do is recognize each grouping of strokes independently from the other strokes in the manager. My idea was to mark the strokes that I would like recognized as selected, and then in the .done() function of the recognizeAsync() method, recursively call the parent function, only on a different set of strokes.

    Below is my current implementation, and it works fine for the first iteration, but on the second, I get an error "WinRTError: A method was called at an unexpected time." I can't really figure out why my code isn't working properly.

     

    Here is my recursive function:

    this.doRecognize = function (stack, context) {
    
    	context.deselectAllManagerStrokes();
    
    	if (queue.length > 1) {
    		var cur_value = stack.pop();
    		var strokes = context.inkManager.getStrokes();
    		
    		//This loop uses some helper methods that determine which InkStroke
    		//objects need to be selected.
    		
    		//select all strokes in the current value
    		for (var k = 0; k < cur_value.strokes.length; k++) {
    			var pq_stroke = context.pqInkStrokes[cur_value.strokes[k]];
    			var ink_stroke_obj = context.inStrokeArray(pq_stroke.stroke, strokes);
    			if (ink_stroke_obj) {
    				ink_stroke_obj.selected = true;
    			}
    		}
    
    		var target = Windows.UI.Input.Inking.InkRecognitionTarget.selected;
    
    		try {
    			context.inkManager.recognizeAsync(target).done(
    				function (results) {
    					// I call this here just to see if a result is returned.
    					console.log(results[0].getTextCandidates()[0]);
    				 
    					// recursively call doRecognize() with the updated
    					// stack of objects. 
    					context.doRecognize(stack, context);
    				},
    				function (error) {
    					var err_str = "RECO ERROR: ";
    
    					if (error.message != undefined) {
    						err_str += error.message;
    					}
    					else if (error.statusText != undefined) {
    						err_str += error.statusText;
    					}
    				}
    			);
    		}
    		catch (e) {
    			console.log(e);
    		}
    	}
    }

    I create a stack of stroke groupings that I would like to recognize, and call the recursive function as follows.

     this.doRecognize(reco_stack, this);

    I'm really stumped as to why this doesn't work as expected. My thought is that it has something to do with how WinJS handles callback functions from asynchronous methods. If this method were synchronous, my desired functionality would be exceptionally easy, but WinJS only offers an asynchronous recognize function.

    Thanks in advance!







    • Edited by Jlange88 Thursday, March 21, 2013 10:31 PM
    Thursday, March 21, 2013 10:28 PM

Answers

  • What line does the exception happen on? What does the call stack look like?

    It seems like recursion would make this unnecessarily memory intensive (lots of extra scope variables that have to be kept around in perpetuity), and harder to debug. Maybe a timeout loop would make things cleaner, here's an example (although I have no way of testing this code, so you might need to debug it a bit):

    this.startRecognition = function () {
        // Keep "this" around for function callbacks to access.
        var context = this;
    
        // indicates that a set of strokes is currently being processed
        context.isRecognizing = false;
    
        // Attempt to recognize new strokes every 50ms, save interval so it can be cleared
        context.recognizeInterval = setInterval(function recognizeStrokes() {
    
            // Don't allow multiple instances of this method to process the same strokes.
            if (context.isRecognizing || queue.length == 0) {
                return;
            }
    
            context.isRecognizing = true; 
            context.deselectAllManagerStrokes();
    
    
            var cur_value = stack.pop();
            var strokes = context.inkManager.getStrokes();
    
            // This loop uses some helper methods that determine which InkStroke
            // objects need to be selected.
    
            // select all strokes in the current value
            for (var k = 0; k < cur_value.strokes.length; k++) {
                var pq_stroke = context.pqInkStrokes[cur_value.strokes[k]];
                var ink_stroke_obj = context.inStrokeArray(pq_stroke.stroke, strokes);
                if (ink_stroke_obj) {
                    ink_stroke_obj.selected = true;
                }
            }
    
            var target = Windows.UI.Input.Inking.InkRecognitionTarget.selected;
    
            try {
                context.inkManager.recognizeAsync(target).done(
                    function recognizeSucceeded(results) {
                        // I call this here just to see if a result is returned.
                        console.log(results[0].getTextCandidates()[0]);
    
                        // Allow recognition to be run again
                        context.isRecognizing = false;
                    },
                    function recognizeFailed(error) {
                        var err_str = "RECO ERROR: ";
    
                        if (error.message != undefined) {
                            err_str += error.message;
                        }
                        else if (error.statusText != undefined) {
                            err_str += error.statusText;
                        }
    
                        // Allow recognition to be run again
                        context.isRecognizing = false;
                    }
                );
            }
            catch (e) {
                console.log(e);
    
                // Allow recognition to be run again
                context.isRecognizing = false;
            }
        }, 50);
    }
    
    this.stopRecognition = function stopRecognition() {
        this.isRecognizing = false;
        clearInterval(this.recognizeInterval);
    };

    • Marked as answer by Jlange88 Monday, April 01, 2013 7:12 PM
    Monday, March 25, 2013 5:54 PM

All replies

  • Can you try using .then rather than .done?

    Matt Small - Microsoft Escalation Engineer - Forum Moderator
    If my reply answers your question, please mark this post as answered.

    NOTE: If I ask for code, please provide something that I can drop directly into a project and run (including XAML), or an actual application project. I'm trying to help a lot of people, so I don't have time to figure out weird snippets with undefined objects and unknown namespaces.

    Friday, March 22, 2013 6:04 PM
    Moderator
  • I have tried using .then() and the result is the same.
    Friday, March 22, 2013 6:07 PM
  • Can you provide a working project for me to debug?

    Matt Small - Microsoft Escalation Engineer - Forum Moderator
    If my reply answers your question, please mark this post as answered.

    NOTE: If I ask for code, please provide something that I can drop directly into a project and run (including XAML), or an actual application project. I'm trying to help a lot of people, so I don't have time to figure out weird snippets with undefined objects and unknown namespaces.

    Monday, March 25, 2013 4:33 PM
    Moderator
  • What line does the exception happen on? What does the call stack look like?

    It seems like recursion would make this unnecessarily memory intensive (lots of extra scope variables that have to be kept around in perpetuity), and harder to debug. Maybe a timeout loop would make things cleaner, here's an example (although I have no way of testing this code, so you might need to debug it a bit):

    this.startRecognition = function () {
        // Keep "this" around for function callbacks to access.
        var context = this;
    
        // indicates that a set of strokes is currently being processed
        context.isRecognizing = false;
    
        // Attempt to recognize new strokes every 50ms, save interval so it can be cleared
        context.recognizeInterval = setInterval(function recognizeStrokes() {
    
            // Don't allow multiple instances of this method to process the same strokes.
            if (context.isRecognizing || queue.length == 0) {
                return;
            }
    
            context.isRecognizing = true; 
            context.deselectAllManagerStrokes();
    
    
            var cur_value = stack.pop();
            var strokes = context.inkManager.getStrokes();
    
            // This loop uses some helper methods that determine which InkStroke
            // objects need to be selected.
    
            // select all strokes in the current value
            for (var k = 0; k < cur_value.strokes.length; k++) {
                var pq_stroke = context.pqInkStrokes[cur_value.strokes[k]];
                var ink_stroke_obj = context.inStrokeArray(pq_stroke.stroke, strokes);
                if (ink_stroke_obj) {
                    ink_stroke_obj.selected = true;
                }
            }
    
            var target = Windows.UI.Input.Inking.InkRecognitionTarget.selected;
    
            try {
                context.inkManager.recognizeAsync(target).done(
                    function recognizeSucceeded(results) {
                        // I call this here just to see if a result is returned.
                        console.log(results[0].getTextCandidates()[0]);
    
                        // Allow recognition to be run again
                        context.isRecognizing = false;
                    },
                    function recognizeFailed(error) {
                        var err_str = "RECO ERROR: ";
    
                        if (error.message != undefined) {
                            err_str += error.message;
                        }
                        else if (error.statusText != undefined) {
                            err_str += error.statusText;
                        }
    
                        // Allow recognition to be run again
                        context.isRecognizing = false;
                    }
                );
            }
            catch (e) {
                console.log(e);
    
                // Allow recognition to be run again
                context.isRecognizing = false;
            }
        }, 50);
    }
    
    this.stopRecognition = function stopRecognition() {
        this.isRecognizing = false;
        clearInterval(this.recognizeInterval);
    };

    • Marked as answer by Jlange88 Monday, April 01, 2013 7:12 PM
    Monday, March 25, 2013 5:54 PM
  • I can put something together after work today. I appreciate the help!
    Monday, March 25, 2013 5:55 PM
  • @Bryan Thomas While adding a timeout was one option, it would essentially make the whole process synchronous which would disrupt user interaction. I don't think that memory is a huge concern at this point, but I definitely understand your point. The reason that I am using recursion is that I only have 1 InkManager instance, and I can't select new InkStroke objects until after the recognition is completed.
    • Edited by Jlange88 Monday, March 25, 2013 6:03 PM
    Monday, March 25, 2013 6:03 PM
  • The reason that I am using recursion is that I only have 1 InkManager instance, and I can't select new InkStroke objects until after the recognition is completed.

    That's what the context.isRecognizing bit handles. It prevents recognition to be run against strokes until inkManager.recognizeAsync returns and you flip the bit back.

    While adding a timeout was one option, it would essentially make the whole process synchronous which would disrupt user interaction

    setInterval, setTimeout, and setImmediate are all async (in fact, setImmediate is the wizard behind the WinJS.Promise curtain). You can tweak the interval delay to tune performance (in case near-constant recognition is slowing down your app).

    Monday, March 25, 2013 6:16 PM
  • I will give your method a try when I create the sample application. It seems odd that this method would work, given that WinJS.Promise uses the setImmediate method. I'll try it, but my initial inclination is that this will fail with the same error as the promise, (essentially we're using setInterval instead of the promise callback). Thank you for your help.
    Monday, March 25, 2013 6:24 PM
  • I apologize for the relative silence on this thread, I was caught up in some other work. Bryan Thomas's answer helped me immensely, and I was able to produce a solution using the interval function. I just wanted to add 1 thing for future reference, I had to replace this

     if (context.isRecognizing || queue.length == 0) {
                return;
            }

    with 

     if (context.isRecognizing || queue.length == 0) {
                context.stopRecognition();
                return;
            }

    Just a simple addition to stop the interval from firing on the base case. I also increased the interval to 100. 

    Thanks a lot everyone!

    Monday, April 01, 2013 7:12 PM
  • I apologize for the relative silence on this thread, I was caught up in some other work. Bryan Thomas's answer helped me immensely, and I was able to produce a solution using the interval function. I just wanted to add 1 thing for future reference, I had to replace this

     if (context.isRecognizing || queue.length == 0) {
                return;
            }

    with 

     if (context.isRecognizing || queue.length == 0) {
                context.stopRecognition();
                return;
            }

    Just a simple addition to stop the interval from firing on the base case. I also increased the interval to 100. 

    Thanks a lot everyone!

    I'm not sure you want to stop recognition if it's currently recognizing. I think you might want to stop if the queue is empty. You might want a separate if statement for the queue so you can call stop from there. For example:

    if (context.isRecognizing) {
        return;
    }
    
    if (queue.length === 0) {
        context.stopRecognition();
        return;
    }



    Monday, April 01, 2013 7:58 PM
  • That is a very good point. I did not run into any problems in my debugging, but I could see that potentially causing issues later on.
    Monday, April 01, 2013 8:02 PM