Source code for azure.cognitiveservices.inkrecognizer._models

#-------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#--------------------------------------------------------------------------

from collections import OrderedDict
from ._enums import InkRecognitionUnitKind, ShapeKind


_STR_MAX_LENGTH = 1024


def _truncate(string):
    if len(string) > _STR_MAX_LENGTH:
        return string[:_STR_MAX_LENGTH] + "..."
    return string


[docs]class Point(object): """ The Point class, unlike the IInkPoint interface, represents a single geometric position on a plane. The point is used to specify the center point of the bounding rectangle of a recognition unit and the key points of a well-formed recognized shape, i.e a beautified shape from Ink Recognizer Service based on the ink shape sent to it. """ def __init__(self, x, y): self._x = x self._y = y @property def x(self): """ X-axis coordinate of the point. """ return self._x @property def y(self): """ X-axis coordinate of the point. """ return self._y
[docs]class Rectangle(object): """ The class represents a rectangle used to identify the boundary of the strokes in a recognition unit. """ def __init__(self, rect_dict): self._x = rect_dict["topX"] self._y = rect_dict["topY"] self._width = rect_dict["width"] self._height = rect_dict["height"] @property def x(self): """ X-axis coordinate of upperleft vertex. """ return self._x @property def y(self): """ Y-axis coordinate of upperleft vertex. """ return self._y @property def width(self): """ Width of the rectangle. """ return self._width @property def height(self): """ Height of the rectangle. """ return self._height
[docs]class InkRecognitionUnit(object): """ An InkRecognitionUnit instance represents a single entity recognized by the Ink Recognizer Service. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.UNKNOWN def __init__(self, json_object): self._id = json_object["id"] self._bounding_box = Rectangle(json_object["boundingRectangle"]) self._rotated_bounding_box = [ Point(point["x"], point["y"]) for point in json_object["rotatedBoundingRectangle"] ] self._stroke_ids = json_object["strokeIds"] self._child_ids = json_object.get("childIds", []) self._parent_id = json_object["parentId"] self._children = [] self._parent = None def __str__(self): return _truncate("%s id=%s strokes=%s" % ( type(self), self._id, self._stroke_ids)) __repr__ = __str__ @property def bounding_box(self): # type: () -> Rectangle """ The bounding box is the rectangular area that contains all the strokes in a recognition unit. :rtype: Rectangle """ return self._bounding_box @property def rotated_bounding_box(self): # type () -> List[Point] """ The rotated bounding box is the oriented rectangular area that contains all the strokes in a recognition unit. Its shape is influenced by the detected orientation of the ink in the recognition unit. It is represented by a list of Points which are the vertices of the rect, clockwise. :rtype: List[Point] """ return self._rotated_bounding_box @property def id(self): # type: () -> int """ Unique identifier for the recognition unit. rtype: int """ return self._id @property def kind(self): # type: () -> InkRecognitionUnitKind """ The kind of the current recognition unit. :rtype: InkRecognitionUnitKind """ return self._ink_recognition_unit_kind @property def stroke_ids(self): # type: () -> List[int] """ Id of strokes of this unit. :rtype: List[int] """ return self._stroke_ids @property def children(self): # type: () -> List[InkRecognitionUnit] """ The children of a recognition unit represent the units contained in a container unit. An example is the relationship between a line and the words on the line. The children of the line is a list of words. "Leaf" units like words which have no children will always return an empty list. :rtype: List[InkRecognitionUnit] """ return self._children @property def parent(self): # type: () -> InkRecognitionUnit """ The parent of a recognition unit represent the unit containing this unit. An example is the relationship between a line and the words on the line. The line is the parent of the words. The top level recognition unit will return None as the parent. :rtype: InkRecognitionUnit or None """ return self._parent
[docs]class InkBullet(InkRecognitionUnit): """ An InkBullet instance represents the collection of one or more ink strokes that were recognized as a bullet point on a line. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.INK_BULLET def __init__(self, json_object): InkRecognitionUnit.__init__(self, json_object) self._recognized_text = json_object.get("recognizedText", "") @property def recognized_text(self): # type: () -> str """ The recognized string of the bullet, e.g. '*'. If the bullet isn't recognized as a string (e.g. when the bullet is a complex shape), an empty string is returned. :rtype: str """ return self._recognized_text
[docs]class InkWord(InkRecognitionUnit): """ An InkWord instance represents the collection of one or more ink strokes that were recognized as a word. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.INK_WORD def __init__(self, json_object): InkRecognitionUnit.__init__(self, json_object) self._recognized_text = json_object.get("recognizedText", "") self._alternates = self._parse_alternates(json_object) def __str__(self): return _truncate("%s id=%s text='%s' strokes=%s" % ( type(self), self._id, self.recognized_text, self._stroke_ids)) def _parse_alternates(self, json_object): # pylint:disable=no-self-use return [json_alternate.get("recognizedString", "") for json_alternate in json_object.get("alternates", [])] @property def alternates(self): # type: () -> List[str] """ A list of alternate strings reported by the service. :rtype: List[str] """ return self._alternates @property def recognized_text(self): # type: () -> str """ The recognized string of the word. If the word isn't recognized, an empty string is returned. :rtype: str """ return self._recognized_text
[docs]class InkDrawing(InkRecognitionUnit): """ An InkDrawing instance represents the collection of one or more ink strokes that were recognized as a drawing/shape. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.INK_DRAWING def __init__(self, json_object): InkRecognitionUnit.__init__(self, json_object) self._center = self._parse_center(json_object.get("center", None)) self._confidence = json_object["confidence"] self._recognized_shape = self._parse_shape(json_object["recognizedObject"]) self._rotated_angle = json_object["rotationAngle"] self._points = self._parse_points(json_object.get("points", [])) self._alternates = self._parse_alternates(json_object) def __str__(self): return _truncate("%s id=%s shape='%s' strokes=%s" % ( type(self), self._id, self.recognized_shape, self._stroke_ids)) def _parse_center(self, center): # pylint:disable=no-self-use, inconsistent-return-statements if center is None: return return Point(center["x"], center["y"]) def _parse_shape(self, shape_string): # pylint:disable=no-self-use try: shape = ShapeKind(shape_string) except ValueError: raise ValueError("Unexpected shape from server: %s." % shape_string) return shape def _parse_points(self, points): # pylint:disable=no-self-use return [Point(point["x"], point["y"]) for point in points] def _parse_one_alternate(self, json_object, json_alternate): # pylint:disable=no-self-use kind = json_alternate.get("category") if kind != InkRecognitionUnitKind.INK_DRAWING.value: raise ValueError("Unexpected InkDrawing alternate kind: %s" % kind) json_object_alternate = json_object.copy() json_object_alternate["confidence"] = json_alternate["confidence"] json_object_alternate["recognizedObject"] = json_alternate["recognizedString"] json_object_alternate["rotationAngle"] = json_alternate["rotationAngle"] json_object_alternate["points"] = json_alternate.get("points", []) json_object_alternate["alternates"] = [] return InkDrawing(json_object_alternate) def _parse_alternates(self, json_object): return [self._parse_one_alternate(json_object, json_alternate) for json_alternate in json_object.get("alternates", [])] @property def center(self): # type: () -> Point """ The center point of the bounding box of the recognition unit as Point. :rtype: Point """ return self._center @property def confidence(self): # type: () -> float """ A number between 0 and 1 which indicates the confidence level in the result. :rtype: float """ return self._confidence @property def recognized_shape(self): # type: () -> ShapeKind """ The ShapeKind enum representing the geometric shape that was recognized. If the drawing isn't one of the known geometric shapes, an ShapeKind.DRAWING is returned. :rtype: ShapeKind """ return self._recognized_shape @property def rotated_angle(self): # type: () -> float """ The angular orientation of an object relative to the horizontal axis. :rtype: float """ return self._rotated_angle @property def points(self): # type: () -> List[Point] """ A list of Point instances that represent points that are relevant to the type of recognition unit. For example, for a leaf node of inkDrawing kind that represents a triangle, points would include the x,y coordinates of the vertices of the recognized triangle. The points represent the coordinates of points used to create the perfectly drawn shape that is closest to the original input. They may not exactly match. :rtype: List[Point] """ return self._points @property def alternates(self): # type: () -> List[InkDrawing] """ A list of alternate InkDrawings when the confidence isn't 1. If this InkDrawing is an alternate, returns None. :rtype: List[InkDrawing] or None """ return self._alternates
[docs]class Line(InkRecognitionUnit): """ A Line instance represents the collection of one or more ink strokes that were recognized as a line. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.LINE def __init__(self, json_object): InkRecognitionUnit.__init__(self, json_object) self._recognized_text = json_object.get("recognizedText", "") self._alternates = self._parse_alternates(json_object.get("alternates", [])) def _parse_alternates(self, alternates): # pylint:disable=no-self-use return [alternate["recognizedString"] for alternate in alternates] @property def alternates(self): # type: () -> List[str] """ A list of alternate strings reported by the service. :rtype: List[str] """ return self._alternates @property def recognized_text(self): # type: () -> str """ The recognized string of the line. If the words in the line are not recognized, an empty string is returned. :rtype: str """ return self._recognized_text @property def bullet(self): # type: () -> Optional[InkBullet, None] """ If the line has a bullet, return it as InkBullet. Otherwise, return None. One line can have at most one bullet. :rtype: InkBullet or None """ for unit in self.children: if unit.kind == InkRecognitionUnitKind.INK_BULLET: return unit return None @property def words(self): # type: () -> List[InkWord] """ All the words in this line. :rtype: List[InkWord] """ return [unit for unit in self.children if unit.kind == InkRecognitionUnitKind.INK_WORD]
[docs]class Paragraph(InkRecognitionUnit): """ A Paragraph instance represents the collection of one or more ink strokes that were recognized as a paragraph. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.PARAGRAPH @property def recognized_text(self): # type: () -> str """ The recognized string of the paragraph. If the words in the paragraph are not recognized, an empty string is returned. :rtype: str """ return "\n".join([child.recognized_text for child in self.children]) @property def lines(self): # type: () -> List[Line] """ All the lines in the paragraph. :rtype: List[Line] """ return [unit for unit in self.children if unit.kind == InkRecognitionUnitKind.LINE] @property def list_items(self): # type: () -> List[ListItem] """ All the ListItems in the paragraph. """ return [unit for unit in self.children if unit.kind == InkRecognitionUnitKind.LIST_ITEM]
[docs]class WritingRegion(InkRecognitionUnit): """ A WritingRegion instance represents a certain part of a writing surface that the user has written at least one word on. WritingRegions are the top-level writing objects under an InkRecognitionRoot. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.WRITING_REGION @property def recognized_text(self): # type: () -> str """ The recognized string of the writing region. If the words in the writing region are not recognized, an empty string is returned. :rtype: str """ return "\n".join([child.recognized_text for child in self.children]) @property def paragraphs(self): # type: () -> List[Paragraph] """ All paragraphs in the writing region. :rtype: List[Paragraph] """ return self.children
[docs]class ListItem(InkRecognitionUnit): """ A ListItem instance represents an item of a list. It has a line which has a bullet at the beginning of it. The list that this ListItem belongs to is not given by Ink Recognizer Service. """ _ink_recognition_unit_kind = InkRecognitionUnitKind.LIST_ITEM @property def recognized_text(self): # type: () -> str """ The recognized string of the list item. If the words in the list item are not recognized, an empty string is returned. :rtype: str """ return "\n".join([child.recognized_text for child in self.children]) @property def lines(self): # type: () -> List[Line] """ All the lines in the list item (Should be only one). :rtype: List[Line] """ return self.children
[docs]class InkRecognitionRoot(object): """ An InkRecognitionRoot instance is the return type from the recognize_ink method in InkRecognizerClient. It is the root of the of recognition unit tree. It also contains the HTTP request status code of the recognition request. WritingRegions and Shapes are the only top-level objects under an InkRecognitionRoot. """ def __init__(self, units): self._units = units self._unit_kind_map = {} for unit in units: if unit.kind in [InkRecognitionUnitKind.INK_DRAWING, InkRecognitionUnitKind.WRITING_REGION]: unit._parent = self # pylint:disable=protected-access def _get_recognition_units(self, unit_kind): if unit_kind in self._unit_kind_map: return self._unit_kind_map[unit_kind] if not isinstance(unit_kind, InkRecognitionUnitKind): raise ValueError("Expected a InkRecognitionUnitKind instance, got %s" % type(unit_kind)) self._unit_kind_map[unit_kind] = [unit for unit in self._units if unit.kind == unit_kind] return self._unit_kind_map[unit_kind] @property def ink_words(self): # type: () -> List[InkWord] """ The list of all InkWord found in the tree returned by the Ink Recognizer Service. :rtype: List[InkWord] """ return self._get_recognition_units(InkRecognitionUnitKind.INK_WORD) @property def ink_drawings(self): # type: () -> List[InkDrawing] """ The list of all InkDrawing found in the tree returned by the Ink Recognizer Service. :rtype: List[InkDrawing] """ return self._get_recognition_units(InkRecognitionUnitKind.INK_DRAWING) @property def lines(self): # type: () -> List[Line] """ The list of all Lines found in the tree returned by the Ink Recognizer Service. :rtype: List[Line] """ return self._get_recognition_units(InkRecognitionUnitKind.LINE) @property def ink_bullets(self): # type: () -> List[InkBullet] """ The list of all InkBullets found in the tree returned by the Ink Recognizer Service. :rtype: List[InkBullet] """ return self._get_recognition_units(InkRecognitionUnitKind.INK_BULLET) @property def paragraphs(self): # type: () -> List[Paragraph] """ The list of all Paragraphs found in the tree returned by the Ink Recognizer Service. :rtype: List[Paragraph] """ return self._get_recognition_units(InkRecognitionUnitKind.PARAGRAPH) @property def writing_regions(self): # type: () -> List[WritingRegion] """ The list of all WritingRegions found in the tree returned by the Ink Recognizer Service. :rtype: List[WritingRegion] """ return self._get_recognition_units(InkRecognitionUnitKind.WRITING_REGION) @property def list_items(self): # type: () -> List[ListItem] """ The list of all ListItems found in the tree returned by the Ink Recognizer Service. :rtype: List[ListItem] """ return self._get_recognition_units(InkRecognitionUnitKind.LIST_ITEM)
[docs] def find_word(self, word): # type: (str) -> List[InkWord] """ Returns all Inkwords returned by the Ink Recognizer Service whose recognized text is exactly the same as the given string. This is case insensitive. :param str word: word to find. :param bool case_sensitive: case sensitive or not. Default is True. :rtype: List[InkWord] """ if not isinstance(word, str): raise ValueError("Expected a string, got %s" % type(word)) word_lower = word.lower() return [ink_word for ink_word in self.ink_words if ink_word.recognized_text.lower() == word_lower]
_RECOGNITION_UNIT_MAP = { InkRecognitionUnitKind.UNKNOWN.value : InkRecognitionUnit, InkRecognitionUnitKind.INK_BULLET.value : InkBullet, InkRecognitionUnitKind.INK_WORD.value : InkWord, InkRecognitionUnitKind.INK_DRAWING.value : InkDrawing, InkRecognitionUnitKind.PARAGRAPH.value : Paragraph, InkRecognitionUnitKind.LINE.value : Line, InkRecognitionUnitKind.WRITING_REGION.value : WritingRegion, InkRecognitionUnitKind.LIST_ITEM.value : ListItem } def _parse_one_recognition_unit(json_object): # find the proper class for the unit and parse it. kind = json_object["category"] if kind in _RECOGNITION_UNIT_MAP: return _RECOGNITION_UNIT_MAP[kind](json_object) # return base class return InkRecognitionUnit(json_object) def _parse_recognition_units(content_json): # parse raw result into InkRecognitionUnit tree units = OrderedDict() recognition_unit_list = content_json.get("recognitionUnits", []) # parse all units in json response into InkRecognitionUnit for json_object in recognition_unit_list: unit = _parse_one_recognition_unit(json_object) units[unit.id] = unit # set children and parent for each unit for unit in units.values(): unit._children = [units[child_id] for child_id in unit._child_ids] # pylint:disable=protected-access unit._parent = units.get(unit._parent_id, None) # pylint:disable=protected-access root = InkRecognitionRoot(units.values()) return root