Source code for handwriting_sample.transformer.interface

import numpy as np
from handwriting_sample.base import HandwritingDataBase


[docs]class HandwritingSampleTransformer(HandwritingDataBase): """Class implementing handwriting data transformer""" # Define conversion variables INCH_TO_MM = 25.4 CM_TO_MM = 10 # Define conversion types LPI = "lpi" LPMM = "lpmm" MM = "mm" # Define device defaults (DTK-1660) LPI_VALUE = 5080 LPMM_VALUE = 200 MM_VALUE = 0.01 MAX_PRESSURE_VALUE = 32767 PRESSURE_LEVELS = 8192 MAX_TILT_VALUE = 900 MAX_AZIMUTH_VALUE = 3600 MAX_TILT_DEGREE = 90 MAX_AZIMUTH_DEGREE = 360 MAX_OLD_RANGE_PRESSURE = 1024
[docs] @staticmethod def normalize_time_series(input_array, max_value): """ Normalizes input time-series. :param input_array: Array of input data :type input_array: nd.array :param max_value: max heoretical value :type max_value: int :return: Normalized data :rtype: list """ # Check input if not all(isinstance(x.item(), (int, float)) for x in input_array): raise ValueError(f"Input data are not numbers!") if not isinstance(max_value, (int, float)): raise ValueError(f"Max value is not number!") # Return normalized array return np.array(list(map((lambda x: x / max_value), input_array)))
[docs] @staticmethod def normalize_pressure(input_array, max_value=MAX_PRESSURE_VALUE, pressure_levels=PRESSURE_LEVELS): """ Normalizes pressure to pressure level of the device. :param input_array: Input array with pressure values :type input_array: np.array :param max_value: OPTIONAL, DEFAULT = 32767 max theoretical raw pressure value :type max_value: int :param pressure_levels: OPTIONAL, DEFAULT = 8192 level of pressure of the device :type pressure_levels: int :return: array with normalized pressure :rtype: np.array """ # Check input if not all(isinstance(x.item(), (int, float)) for x in input_array): raise ValueError(f"Input data are not numbers!") if not isinstance(max_value, (int, float)): raise ValueError(f"Max value is not number!") if not isinstance(pressure_levels, (int, float)): raise ValueError(f"Pressure levels is not number!") return np.array(list(map((lambda x: (x / max_value) * pressure_levels), input_array)))
[docs] @staticmethod def transform_time_to_seconds(time_array): """ Transforms time to seconds. :param time_array: input array of timestamp :type time_array: np.array :return: time in seconds :rtype: nd.array """ # Check input if not all(isinstance(x.item(), (int, float)) for x in time_array): raise ValueError(f"Input data are not numbers!") return np.array(list(map((lambda x: (x - time_array[0]) / 1e3), time_array)))
[docs] @staticmethod def transform_angle(input_array, max_raw_value, max_degree_value): """ Transforms raw angle to degrees. :param input_array: Input array with raw angle values :type input_array: np.array :param max_raw_value: Maximal theoretical value of raw angle :type max_raw_value: int :param max_degree_value: Maximal value of angle in degrees :type max_degree_value: int :return: angle in degrees :rtype: nd.array """ # Check input if not all(isinstance(x.item(), (int, float)) for x in input_array): raise ValueError(f"Input data are not numbers!") if not isinstance(max_raw_value, (int, float)): raise ValueError(f"Max raw value is not number!") if not isinstance(max_degree_value, (int, float)): raise ValueError(f"Max angle value is not number!") # Get value of degree per one point degree_per_point = max_degree_value / max_raw_value # Transform array to degrees return np.array(list(map((lambda x: (x * degree_per_point)), input_array)))
[docs] def transform_axis(self, sample, conversion_type=LPI, lpi_value=LPI_VALUE, lpmm_value=LPMM_VALUE, shift_to_zero=True): """ Transforms X,Y axis to millimeters. :param sample: object of HandwritingSample class :type sample: handwriting_sample.HandwritingSample :param conversion_type: OPTIONAL ["lpi"|"lpmm"|"mm"], DEFAULT="lpi". Set the capturing method used for mapping; "lpi" for inch; "lpmm" for millimeters; "mm" for direct to millimeters :type conversion_type: str :param lpi_value: OPTIONAL, DEFAULT = 5080 Set lpi value of digitizing tablet :type lpi_value: int :param lpmm_value: OPTIONAL, DEFAULT = 200 Set lpmm value of digitizing tablet :type lpmm_value: int :param shift_to_zero: OPTIONAL, DEFAULT = True Shift axis values to start from 0,0 coordinates :type shift_to_zero: bool :return: updated object of HandwritingSample class :rtype: handwriting_sample.HandwritingSample """ # Check input if not isinstance(conversion_type, str): raise ValueError(f"Conversion type must be string not {type(conversion_type)}.") if not isinstance(lpi_value, int): raise ValueError(f"LPI value must be int not {type(lpi_value)}.") if not isinstance(lpmm_value, int): raise ValueError(f"LPMM value must be int not {type(lpmm_value)}.") # Check for conversion type if conversion_type == self.LPI: self.log(f"Using {conversion_type} = {lpi_value} for axis conversion to millimeters.") # Convert axis sample.x = (sample.x * self.INCH_TO_MM) / lpi_value sample.y = (sample.y * self.INCH_TO_MM) / lpi_value # BAD FORMULA... MAKING NO SENSE! # elif conversion_type == self.LPMM: # # self.log(f"Using {conversion_type} = {lpmm_value} for axis conversion to millimeters.") # # # Convert axis # sample.x = (sample.x * self.INCH_TO_MM) / lpmm_value # sample.y = (sample.y * self.INCH_TO_MM) / lpmm_value elif conversion_type == self.LPMM: raise NotImplementedError(f"Do not supporting this conversion anymore, due to incorrect formula.") elif conversion_type == self.MM: self.log(f"Using {conversion_type} = {self.MM_VALUE} for axis conversion to millimeters.") # Convert axis sample.x = sample.x * self.MM_VALUE sample.y = sample.y * self.MM_VALUE else: raise ValueError(f"Unknown conversion type {conversion_type}") if shift_to_zero: self.log(f"Shift axis data to start from 0,0 coordinates") sample.x = sample.x - min(sample.x) sample.y = sample.y - min(sample.y) return sample
[docs] def transform_all_units( self, sample, conversion_type=LPI, lpi_value=LPI_VALUE, lpmm_value=LPMM_VALUE, max_raw_azimuth=MAX_AZIMUTH_VALUE, max_raw_tilt=MAX_TILT_VALUE, max_degree_azimuth=MAX_AZIMUTH_DEGREE, max_degree_tilt=MAX_TILT_DEGREE, max_pressure=MAX_PRESSURE_VALUE, pressure_levels=PRESSURE_LEVELS, angles_to_degrees=True, shift_to_zero=True): """ Transforms all unites of sample object: - transforms X,Y to millimeters. - transform time to seconds - normalize or transform to degrees angles - normalize pressure :param sample: updated object of HandwritingSample class :type sample: handwriting_sample.HandwritingSample :param conversion_type: OPTIONAL ["lpi"|"lpmm"|"mm"], DEFAULT="lpi". Set the capturing method used for mapping; "lpi" for inch; "lpmm" for millimeters; "mm" for direct to millimeters :type conversion_type: str :param lpi_value: OPTIONAL , DEFAULT = 5080 Set lpi value of digitizing tablet. :type lpi_value: int :param lpmm_value: OPTIONAL, DEFAULT = 200 Set lpmm value of digitizing tablet. :type lpmm_value: int :param max_raw_azimuth: OPTIONAL, DEFAULT = 3600 Maximum theoretical value of azimuth. :type max_raw_azimuth: int :param max_raw_tilt: OPTIONAL, DEFAULT = 900 Maximum theoretical value of tilt. :type max_raw_tilt: int :param max_degree_azimuth: OPTIONAL, DEFAULT = 360 Maximum degree value of azimuth. :type max_degree_azimuth: int :param max_degree_tilt: OPTIONAL, DEFAULT = 90 Maximum degree value of tilt. :type max_degree_tilt: int :param max_pressure: OPTIONAL, DEFAULT = 32767 Maximum theoretical value of pressure. :type max_pressure: int :param pressure_levels: OPTIONAL, DEFAULT = 8192 Level of pressures of the device. :type pressure_levels: int :param angles_to_degrees: OPTIONAL, DEFAULT = True Transform angles to degrees :type angles_to_degrees: bool :param shift_to_zero: OPTIONAL, DEFAULT = True Shift axis values to start from 0,0 coordinates :type shift_to_zero: bool :return: updated object of HandwritingSample class :rtype: handwriting_sample.HandwritingSample """ # TODO _read max/range values from metadata sample = self.transform_axis( sample, conversion_type=conversion_type, lpi_value=lpi_value, lpmm_value=lpmm_value, shift_to_zero=shift_to_zero) # Transform time to seconds sample.time = self.transform_time_to_seconds(sample.time) # Normalize Azimuth, Tilt or transform to degree if angles_to_degrees: # Transform to degrees sample.azimuth = self.transform_angle( sample.azimuth, max_raw_azimuth, max_degree_azimuth) sample.tilt = self.transform_angle( sample.tilt, max_raw_tilt, max_degree_tilt) # Normalize pressure sample.pressure = self.normalize_pressure( sample.pressure, max_value=max_pressure, pressure_levels=pressure_levels) # Return return sample
[docs] def control_for_pressure( self, input_array, pressure_levels=PRESSURE_LEVELS, max_raw_press_value=MAX_PRESSURE_VALUE, max_range_press=MAX_OLD_RANGE_PRESSURE): """ Controls for pressure range values. This function is a fix pressure function in case the old driver has been used Old pressure range is from 0-1024 New and correct pressure range is 0-32767 :param input_array: input pressure data :type input_array: np.array :param pressure_levels: OPTIONAL - level of pressures of the device. DEFAULT = 8192 :type pressure_levels: int :param max_raw_press_value: OPTIONAL - maximum theoretical value of pressure. DEFAULT = 32767 :type max_raw_press_value: int :param max_range_press: OPTIONAL - maximum allowed pressure range of raw data. DEFAULT = 1024 :type max_range_press: int :return: converted pressure to new scale :rtype: np.array """ data_pressure_range = np.ptp(input_array) self.log(f"Pressure range of data is: {data_pressure_range}") if data_pressure_range > max_range_press: # Convert the pressure to lower range self.log(f"Maximum allowed pressure range is: {max_range_press}.") self.log(f"Converting pressure values with following params:") self.log(f" - device max pressure level = {pressure_levels}") self.log(f" - raw data max pressure value = {max_raw_press_value}") output = np.array(list(map(lambda x: round((x / max_raw_press_value) * max_range_press), input_array))) print(f"Converted Pressure range is: {np.ptp(output)}") return output return input_array
[docs] @staticmethod def correct_pen_status(sample): """ Corrects pen status values to binary form :param sample: updated object of HandwritingSample class :type sample: handwriting_sample.HandwritingSample :return: instance of HandwritingSample :rtype: HandwritingSample """ # Correct pen status sample.pen_status = np.array(list(map(lambda x: 1 if x > 0 else 0, sample.pressure))) # Hold meta data meta_data = sample.meta # Validate sample and return r_sample = sample.from_pandas_dataframe(sample.data_pandas_dataframe) # Put back teh meta data r_sample.add_meta_data(meta_data) return r_sample
[docs] @staticmethod def revert_axis(input_array, axis_max_value): """ Revert axis :param input_array: Input array with raw angle values :type input_array: np.array :return: reverted axis :rtype: nd.array """ # Check input if not all(isinstance(x.item(), (int, float)) for x in input_array): raise ValueError(f"Input data are not numbers!") if not isinstance(axis_max_value, (int, float)): raise ValueError(f"Axis max value is not a number!") if max(input_array) > axis_max_value: raise ValueError(f"Axis max value ({axis_max_value}) is lower than max value of the input array" f" ({max(input_array)})! ") # Revert axis return np.array(list(map(lambda x: axis_max_value - x, input_array)))
[docs] @staticmethod def rescale_axis(sample, rescale_coef=0.5): """ Rescale axis values :param sample: updated object of HandwritingSample class :type sample: handwriting_sample.HandwritingSample :param rescale_coef: OPTIONAL - Coefficient of rescaling, DEFAULT = 0.5 :type rescale_coef: float :return: handwriting sample with rescaled axis :rtype: handwriting_sample.HandwritingSample """ # Check input if not isinstance(rescale_coef, (int, float)): raise ValueError(f"Coefficient of rescaling is not number!") sample.x = sample.x * rescale_coef sample.y = sample.y * rescale_coef return sample