import numpy
import functools
from handwriting_sample import HandwritingSample
from handwriting_features.data.utils.math import derivation
from handwriting_features.data.exceptions.sample import *
[docs]class HandwritingSampleWrapper(object):
"""Class implementing the handwriting sample wrapper"""
# Handwriting data axes
axes = ("x", "y", "xy")
# Handwriting data surface information
surfaces = ("on_surface", "in_air")
def __init__(self, sample):
"""Constructor method"""
# Set the sample
self.sample = sample
# Set the strokes
self.strokes = self.sample.get_strokes()
# ------------------------------- #
# Alternative constructor methods #
# ------------------------------- #
[docs] @classmethod
def from_list(cls, values, labels=None):
"""
Initializes HandwritingSampleWrapper object from a list.
:param values: data values
:type values: list
:param labels: labels for the data values
:type labels: list, optional
:return: HandwritingSampleWrapper object
:rtype: HandwritingSampleWrapper
"""
return cls(HandwritingSample.from_list(values, labels))
[docs] @classmethod
def from_numpy_array(cls, values, labels=None):
"""
Initializes HandwritingSampleWrapper object from a numpy array.
:param values: data values
:type values: numpy.ndarray
:param labels: labels for the data values
:type labels: list, optional
:return: HandwritingSampleWrapper object
:rtype: HandwritingSampleWrapper
"""
return cls(HandwritingSample.from_numpy_array(values, labels))
[docs] @classmethod
def from_pandas_dataframe(cls, values, labels=None):
"""
Initializes HandwritingSampleWrapper object from a pandas DataFrame.
:param values: data values
:type values: pandas.DataFrame
:param labels: labels for the data values
:type labels: list, optional
:return: HandwritingSampleWrapper object
:rtype: HandwritingSampleWrapper
"""
return cls(HandwritingSample.from_pandas_dataframe(values, labels))
[docs] @classmethod
def from_json(cls, path, labels=None):
"""
Initializes HandwritingSampleWrapper object from a JSON file.
:param path: path to a JSON file
:type path: str
:param labels: labels for the data values
:type labels: list, optional
:return: HandwritingSampleWrapper object
:rtype: HandwritingSampleWrapper
"""
return cls(HandwritingSample.from_json(path, labels))
[docs] @classmethod
def from_svc(cls, path, labels=None):
"""
Initializes HandwritingSampleWrapper object from an SVC file.
:param path: path to an SVC file
:type path: str
:param labels: labels for the data values
:type labels: list, optional
:return: HandwritingSampleWrapper object
:rtype: HandwritingSampleWrapper
"""
return cls(HandwritingSample.from_svc(path, labels))
# ----------------------------- #
# Derived handwriting variables #
# ----------------------------- #
[docs] @functools.lru_cache(maxsize=len(axes) * len(surfaces))
def compute_velocity(self, axis="xy", in_air=False):
"""
Computes the velocity.
:param axis: axis to compute the velocity from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: velocity
:rtype: numpy.ndarray or numpy.NaN
"""
# Validate the input arguments
self.validate_axis(axis)
self.validate_surface_movement(in_air)
# Compute the velocity
velocity = numpy.concatenate(self._compute_strokes_velocities(axis, in_air))
# Return the velocity
return velocity if velocity is not None and velocity.size > 0 else numpy.nan
[docs] @functools.lru_cache(maxsize=len(axes) * len(surfaces))
def compute_acceleration(self, axis="xy", in_air=False):
"""
Computes the acceleration.
:param axis: axis to compute the acceleration from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: acceleration
:rtype: numpy.ndarray or numpy.NaN
"""
# Validate the input arguments
self.validate_axis(axis)
self.validate_surface_movement(in_air)
# Compute the acceleration
acceleration = numpy.concatenate(self._compute_strokes_accelerations(axis, in_air))
# Return the acceleration
return acceleration if acceleration is not None and acceleration.size > 0 else numpy.nan
[docs] @functools.lru_cache(maxsize=len(axes) * len(surfaces))
def compute_jerk(self, axis="xy", in_air=False):
"""
Computes the jerk.
:param axis: axis to compute the jerk from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: jerk
:rtype: numpy.ndarray or numpy.NaN
"""
# Validate the input arguments
self.validate_axis(axis)
self.validate_surface_movement(in_air)
# Compute the jerk
jerk = numpy.concatenate(self._compute_strokes_jerks(axis, in_air))
# Return the jerk
return jerk if jerk is not None and jerk.size > 0 else numpy.nan
[docs] @functools.lru_cache(maxsize=len(surfaces))
def compute_azimuth(self, in_air=False):
"""
Computes the azimuth.
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: azimuth
:rtype: numpy.ndarray or numpy.NaN
"""
# Validate the input arguments
self.validate_surface_movement(in_air)
# Get the data to be used for the feature computation
data = self.on_surface_data if not in_air else self.in_air_data
# Return the azimuth
return data.azimuth
[docs] @functools.lru_cache(maxsize=len(surfaces))
def compute_tilt(self, in_air=False):
"""
Computes the tilt.
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: tilt
:rtype: numpy.ndarray or numpy.NaN
"""
# Validate the input arguments
self.validate_surface_movement(in_air)
# Get the data to be used for the feature computation
data = self.on_surface_data if not in_air else self.in_air_data
# Return the tilt
return data.tilt
[docs] @functools.lru_cache(maxsize=1)
def compute_pressure(self):
"""
Computes the pressure.
:return: tilt
:rtype: numpy.ndarray or numpy.NaN
"""
return self.on_surface_data.pressure
# ---------------------------- #
# Sample handwriting variables #
# ---------------------------- #
@property
def sample_x(self):
return self.sample.x
@property
def sample_y(self):
return self.sample.y
@property
def sample_time(self):
return self.sample.time
@property
def sample_pen_status(self):
return self.sample.pen_status
@property
def sample_azimuth(self):
return self.sample.azimuth
@property
def sample_tilt(self):
return self.sample.tilt
@property
def sample_pressure(self):
return self.sample.pressure
@functools.cached_property
def in_air_data(self):
return self.sample.get_in_air_data()
@functools.cached_property
def on_surface_data(self):
return self.sample.get_on_surface_data()
@functools.cached_property
def in_air_strokes(self):
return [stroke for status, stroke in self.strokes if status == "in_air"]
@functools.cached_property
def on_surface_strokes(self):
return [stroke for status, stroke in self.strokes if status == "on_surface"]
# ------------------- #
# Validation routines #
# ------------------- #
[docs] @classmethod
def validate_axis(cls, axis):
"""Validates the axis"""
if axis not in cls.axes:
raise UnsupportedAxisError(f"Unsupported <axis> argument {axis}; must be in {cls.axes}")
[docs] @classmethod
def validate_surface_movement(cls, in_air):
"""Validates the surface movement"""
if not isinstance(in_air, bool):
raise UnsupportedSurfaceMovementError(f"Unsupported <in_air> argument {in_air}; must be bool")
# ---------------------- #
# Computational routines #
# ---------------------- #
@functools.lru_cache(maxsize=len(axes) * len(surfaces))
def _compute_strokes_trajectories(self, axis="xy", in_air=False):
"""
Computes the strokes trajectories.
:param axis: axis to compute the trajectories from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: strokes trajectories
:rtype: list
"""
# Get the strokes
strokes = self.on_surface_strokes if not in_air else self.in_air_strokes
# Return the strokes trajectories
if axis == "x":
return [numpy.abs(derivation(stroke.x)) for stroke in strokes]
if axis == "y":
return [numpy.abs(derivation(stroke.y)) for stroke in strokes]
if axis == "xy":
return [
numpy.sqrt(numpy.power(derivation(stroke.x), 2) + numpy.power(derivation(stroke.y), 2))
for stroke in strokes
]
@functools.lru_cache(maxsize=len(surfaces))
def _compute_strokes_time_differences(self, in_air=False):
"""
Computes the strokes time differences.
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: strokes time differences
:rtype: list
"""
return [
derivation(stroke.time)
for stroke in (self.on_surface_strokes if not in_air else self.in_air_strokes)
]
@functools.lru_cache(maxsize=len(axes) * len(surfaces))
def _compute_strokes_velocities(self, axis="xy", in_air=False):
"""
Computes the strokes velocities.
:param axis: axis to compute the velocities from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: strokes velocities
:rtype: list
"""
# Get the variable to differentiate over (strokes trajectories)
ds = self._compute_strokes_trajectories(axis, in_air)
# Get the stroke time differences
dt = self._compute_strokes_time_differences(in_air)
# Return the stokes velocities
return [d / t for (d, t) in zip(ds, dt)]
@functools.lru_cache(maxsize=len(axes) * len(surfaces))
def _compute_strokes_accelerations(self, axis="xy", in_air=False):
"""
Computes the strokes accelerations.
:param axis: axis to compute the accelerations from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: strokes accelerations
:rtype: list
"""
# Get the variable to differentiate over (strokes velocities)
dv = self._compute_strokes_velocities(axis, in_air)
# Get the stroke time differences
dt = self._compute_strokes_time_differences(in_air)
# Return the stokes accelerations
return [derivation(d) / t[1:] for (d, t) in zip(dv, dt)]
@functools.lru_cache(maxsize=len(axes) * len(surfaces))
def _compute_strokes_jerks(self, axis="xy", in_air=False):
"""
Computes the strokes jerks.
:param axis: axis to compute the jerks from, defaults to "xy"
:type axis: str, optional
:param in_air: in-air flag, defaults to False
:type in_air: bool, optional
:return: strokes jerks
:rtype: list
"""
# Get the variable to differentiate over (strokes accelerations)
da = self._compute_strokes_accelerations(axis, in_air)
# Get the stroke time differences
dt = self._compute_strokes_time_differences(in_air)
# Return the stokes jerks
return [derivation(d) / t[2:] for (d, t) in zip(da, dt)]