Source code for pedarProbe.export
"""Analysis result export functionalities. Short-cut functions are realised in :class:`pedarProbe.node.PedarNode` to facilitate the usability.
"""
from __future__ import annotations
from typing import Type, Union
import os
import copy
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image, ImageOps
import pedarProbe
from pedarProbe import node
[docs]def attribute_batch_export(node, attr_name: str, export_layer: str, export_folder='output', save_suffix: str = ''):
"""Batch export the analysis result of a specific layer in the node tree as a :code:`.xlsx` file.
Parameters
---
attr_name
attribute name, which is a keyword in :class:`~pedarProbe.node.PedarNode`'s :attr:`attributes` dictionary.
export_layer
if export as local file, the name of the layer to export.
export_folder
the folder of the exported file.
save_suffix
the suffix added to the default export file name :code:`sensor_pti`.
.. tip::
A specific suffix can avoid exported file be override by future export.
"""
export_level = node.loc_map[export_layer]
row_name_list = []
attribute_list = []
export_nodes = node.collect_layer(export_layer, nodes=[])
for node in export_nodes:
row_name = ' '.join(node.loc[1 : export_level + 1])
row_name_list.append(row_name)
attribute_list.append(node.attribute[attr_name])
# rearrange as a data frame and export
df_condition = pd.DataFrame({"condition": row_name_list})
df_data = pd.concat(attribute_list, axis=1).transpose()
df_export = pd.concat([df_condition, df_data], axis=1)
df_export.to_excel("{}/{}{}.xlsx".format(export_folder, attr_name, save_suffix))
[docs]class FootHeatmap(object):
"""Foot heatmap class.
Parameters
---
node
the node that stores the data for generate foot heatmap.
attr_name
attribute name, which is a keyword in :class:`~pedarProbe.node.PedarNode`'s :attr:`attributes` dictionary.
Note
---
`Class Attributes`
self.l_pedar :class:`numpy.ndarray`
left foot attribute distribution.
self.r_pedar
left foot attribute distribution.
"""
l_index = None
"""
:class:`dict` that stores the pixel coordinates of different left foot sensor areas, which sensor ID 0~98 as the keywords. Initialised as :code:`None`.
"""
r_index = None
"""
:class:`dict` that stores the pixel coordinates of different right foot sensor areas, which sensor ID 99~197 as the keywords. Initialised as :code:`None`.
"""
[docs] def __init__(self, node: Type[node.Node], attr_name: str = 'sensor_peak'):
# if foot mask has never been loaded, call load_foot_mask() method
if self.l_index is None:
self.load_foot_mask()
self.fill_foot_heat_map(node, attr_name)
[docs] @classmethod
def load_foot_mask(cls):
"""Load foot mask, detect the pixel coordinates belonging to 0~198 sensor areas, and store them in :attr:`cls.l_index` and :attr:`cla.r_index`.
Tip
---
Every pixel of the left foot mask file :code:`config/left_foot_mask.png` is labelled by the pedar sensor ID it belongs to. The right foot mask is generated by flipping the left foot mask.
Noted that there are two sensor ID numbering conventions. Please refer to :meth:`~pedarProbe.parse.Pedar_asc.id_map` for more information.
Attention
---
Being called in the first instantiating of :class:`FootHeatmap` by :meth:`__init__`.
"""
mod_path = os.path.dirname(pedarProbe.__file__)
mask_dir = os.path.join(mod_path, 'config/left_foot_mask.png')
# load the left foot mask image
# flip it as the right foot mask image
l_img = Image.open(mask_dir)
r_img = ImageOps.mirror(l_img)
l_mask = np.array(l_img).astype(np.float64)
r_mask = np.array(r_img).astype(np.float64)
cls.l_shape = l_mask.shape
cls.r_shape = r_mask.shape
# detect pixels of area no.1~197 and store the corresponding indexes
cls.l_index = {}
cls.r_index = {}
for n in range(0, 198):
cls.l_index[n] = np.where(l_mask == n + 1)
cls.r_index[n + 99] = np.where(r_mask == n + 1)
[docs] def fill_foot_heat_map(self, node: Type[node.Node], attr_name: str = 'sensor_peak'):
"""Fill attribute value to distribution according to :attr:`cls.l_index` and :attr:`cla.r_index`.
Parameters
---
node
the node that stores the data for generate foot heatmap.
attr_name
attribute name, which is a keyword in :class:`~pedarProbe.node.PedarNode`'s :attr:`attributes` dictionary.
Attention
---
called by :meth:`__init__`.
"""
# create empty left and right distribution
self.l_pedar = np.zeros(self.l_shape)
self.r_pedar = np.zeros(self.r_shape)
# fill the attribute distribution
for n in node.attribute[attr_name].index:
if n <= 99:
# filling left foot area
self.l_pedar[self.l_index[n]] = node.attribute[attr_name][n]
else:
# filling right foot area
self.r_pedar[self.r_index[n]] = node.attribute[attr_name][n]
[docs] def export_foot_heatmap(self, range: Union[str, tuple] = 'static', is_export: bool = True, export_folder: str = 'output', save_suffix: str = ''):
"""Plot and export the foot heatmap.
Parameters
---
range
The color mapping range.
- :code:`'static'`: map value :math:`\in [0, 300]` to color, suitable for pressure distribution heatmap.
- :code:`'auto'`: automatically detect the value range, suitable for other attribute distribution heatmap.
- manually setting the color mapping range with a :class:`tuple` in the format of :code:`(<min, max>)`, suitable for generating various heatmap with a unified color mapping range.
.. attention::
Value lower (higher) than the lower (higher) range will be mapped as the lowest (highest) color in the color bar.
is_export
export the analysed result as a local file or not.
export_folder
the folder of the exported file.
save_suffix
the suffix added to the default export file name :code:`foot_heatmap`.
.. tip::
A specific suffix can avoid exported file be override by future export.
"""
# value range
if range == 'static':
range_min = 0
range_max = 300
elif range == 'auto':
range_min = np.min([np.min(self.l_pedar), np.min(self.r_pedar)])
range_max = np.max([np.max(self.l_pedar), np.max(self.r_pedar)])
else:
# manually control with a tuple of (min, max)
range_min, range_max = range
# show and export as heatmap
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(7, 8))
l_img = axes[0].imshow(self.l_pedar, cmap='cool', vmin=range_min, vmax=range_max)
r_img = axes[1].imshow(self.r_pedar, cmap='cool', vmin=range_min, vmax=range_max)
fig.subplots_adjust(right=0.85)
cbar_ax = fig.add_axes([0.88, 0.15, 0.04, 0.7])
fig.colorbar(r_img, cbar_ax)
plt.show()
if is_export:
fig.savefig('{}/foot_heatmap{}'.format(export_folder, save_suffix))
def __add__(self, other: Type[FootHeatmap]) -> Type[FootHeatmap]:
"""Magic method of adding."""
new_hm = copy.deepcopy(self)
new_hm.l_pedar += other.l_pedar
new_hm.r_pedar += other.r_pedar
return new_hm
def __sub__(self, other: Type[FootHeatmap]) -> Type[FootHeatmap]:
"""Magic method of subtraction."""
new_hm = copy.deepcopy(self)
new_hm.l_pedar -= other.l_pedar
new_hm.r_pedar -= other.r_pedar
return new_hm
def __mul__(self, val: Union[float, int]) -> Type[FootHeatmap]:
"""Magic method of multiplication."""
new_hm = copy.deepcopy(self)
new_hm.l_pedar *= val
new_hm.r_pedar *= val
return new_hm
def __truediv__(self, val: Union[float, int]) -> Type[FootHeatmap]:
"""Magic method of true division."""
new_hm = copy.deepcopy(self)
new_hm.l_pedar /= val
new_hm.r_pedar /= val
return new_hm
[docs] @staticmethod
def average(*hms):
"""Return an averaged heatmap object of the inputted heatmap objects.
Parameters
---
*hms
inputted heatmap objects for average.
"""
# start parameter of sum() is an int, which cause issue
# therefore set the start of sum as hms[0] and the others as hms[1:]
return sum(hms[1:], start=hms[0]) / len(hms)