diff --git a/Makefile b/Makefile deleted file mode 100644 index a41fc30..0000000 --- a/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -# formatting - -fmt-black: - poetry run black pose-classification-kit/ - -lint-black: - poetry run black --check pose-classification-kit/src/ - -lint: lint-black diff --git a/pose_classification_kit/__init__.py b/pose_classification_kit/__init__.py index c72e379..9b102be 100644 --- a/pose_classification_kit/__init__.py +++ b/pose_classification_kit/__init__.py @@ -1 +1 @@ -__version__ = "1.1.4" +__version__ = "1.1.5" diff --git a/pose_classification_kit/datasets/__init__.py b/pose_classification_kit/datasets/__init__.py index 405c628..8b9b803 100644 --- a/pose_classification_kit/datasets/__init__.py +++ b/pose_classification_kit/datasets/__init__.py @@ -7,25 +7,21 @@ from .data_augmentation import dataAugmentation -def importBodyCSVDataset(testSplit): +def importBodyCSVDataset(testSplit: float, local_import: bool): """Import body dataset as numpy arrays from GitHub if available, or local dataset otherwise. Args: testSplit (float, optional): Percentage of the dataset reserved for testing. Defaults to 0.15. Must be between 0.0 and 1.0. """ + assert 0.0 <= testSplit <= 1.0 + datasetPath = DATASETS_PATH / "BodyPose_Dataset.csv" datasetURL = "https://raw.githubusercontent.com/ArthurFDLR/pose-classification-kit/master/pose_classification_kit/datasets/BodyPose_Dataset.csv" - assert 0.0 <= testSplit <= 1.0 - - # Try to fetch the most recent dataset, load local file otherwise. - try: - dataset_df = pd.read_csv(datasetURL) - print("Dataset loaded from", datasetURL) - except: - assert datasetPath.is_file(), "No local dataset found." + if local_import: dataset_df = pd.read_csv(datasetPath) - print("Dataset loaded from", str(datasetPath)) + else: + dataset_df = pd.read_csv(datasetURL) bodyLabels_df = dataset_df.groupby("label") labels = list(dataset_df.label.unique()) @@ -63,7 +59,10 @@ def importBodyCSVDataset(testSplit): def bodyDataset( - testSplit: float = 0.15, shuffle: bool = True, bodyModel: BodyModel = BODY25 + testSplit: float = 0.15, + shuffle: bool = True, + bodyModel: BodyModel = BODY25, + local_import: bool = False, ): """Return the dataset of body keypoints (see pose_classification_kit/datasets/BodyPose_Dataset.csv) as numpy arrays. @@ -72,6 +71,7 @@ def bodyDataset( testSplit (float, optional): Percentage of the dataset reserved for testing. Defaults to 0.15. Must be between 0.0 and 1.0. shuffle (bool, optional): Shuffle the whole dataset. Defaults to True. bodyModel (BodyModel, optional): Select the keypoint format of the dataset. BODY25 or BODY18. Defaults to BODY25. + local_import (bool, optional): Choose to use local dataset or fetch online dataset (global repository). Default False. Returns: dict: { @@ -85,7 +85,9 @@ def bodyDataset( } """ - x_train, x_test, y_train, y_test, labels = importBodyCSVDataset(testSplit) + x_train, x_test, y_train, y_test, labels = importBodyCSVDataset( + testSplit, local_import + ) # Shuffle in unison if shuffle: @@ -121,7 +123,12 @@ def bodyDataset( } -def handDataset(testSplit: float = 0.15, shuffle: bool = True, handID: int = 0): +def handDataset( + testSplit: float = 0.15, + shuffle: bool = True, + handID: int = 0, + local_import: bool = False, +): """Return the dataset of hand keypoints (see pose_classification_kit/datasets/HandPose_Dataset.csv) as numpy arrays. @@ -129,6 +136,7 @@ def handDataset(testSplit: float = 0.15, shuffle: bool = True, handID: int = 0): testSplit (float, optional): Percent of the dataset reserved for testing. Defaults to 0.15. Must be between 0.0 and 1.0. shuffle (bool, optional): Shuffle the whole dataset. Defaults to True. handID (int, optional): Select hand side - 0:left, 1:right. Default to 0. + local_import (bool, optional): Choose to use local dataset or fetch online dataset (global repository). Default False. Returns: dict: { @@ -141,19 +149,15 @@ def handDataset(testSplit: float = 0.15, shuffle: bool = True, handID: int = 0): 'labels': list of labels } """ + assert 0.0 <= testSplit <= 1.0 + datasetPath = DATASETS_PATH / "HandPose_Dataset.csv" datasetURL = "https://raw.githubusercontent.com/ArthurFDLR/pose-classification-kit/master/pose_classification_kit/datasets/HandPose_Dataset.csv" - assert 0.0 <= testSplit <= 1.0 - - # Try to fetch the most recent dataset, load local file otherwise. - try: - dataset_df = pd.read_csv(datasetURL) - print("Dataset loaded from", datasetURL) - except: - assert datasetPath.is_file(), "No local dataset found." + if local_import: dataset_df = pd.read_csv(datasetPath) - print("Dataset loaded from", str(datasetPath)) + else: + dataset_df = pd.read_csv(datasetURL) hand_label = "right" if handID else "left" handLabels_df = { diff --git a/pose_classification_kit/datasets/body_models.py b/pose_classification_kit/datasets/body_models.py index 124c79b..fdb81c3 100644 --- a/pose_classification_kit/datasets/body_models.py +++ b/pose_classification_kit/datasets/body_models.py @@ -93,27 +93,27 @@ def __init__(self, mapping, pairs) -> None: "neck_y", ], pairs=[ - [(30,31), (26,27)], - [(26,27), (22,23)], - [(32,33), (28,29)], - [(28,29), (24,25)], - [(22,23), (24,25)], - [(10,11), (14,15)], - [(12,13), (16,17)], - [(14,15), (18,19)], - [(16,17), (20,21)], - [(2,3), (4,5)], - [(0,1), (2,3)], - [(0,1), (4,5)], - [(2,3), (6,7)], - [(4,5), (8,9)], - [(6,7), (10,11)], - [(8,9), (12,13)], - [(34,35), (0,1)], - [(34,35), (10,11)], - [(34,35), (12,13)], - [(34,35), (22,23)], - [(34,35), (24,25)], + [(30, 31), (26, 27)], + [(26, 27), (22, 23)], + [(32, 33), (28, 29)], + [(28, 29), (24, 25)], + [(22, 23), (24, 25)], + [(10, 11), (14, 15)], + [(12, 13), (16, 17)], + [(14, 15), (18, 19)], + [(16, 17), (20, 21)], + [(2, 3), (4, 5)], + [(0, 1), (2, 3)], + [(0, 1), (4, 5)], + [(2, 3), (6, 7)], + [(4, 5), (8, 9)], + [(6, 7), (10, 11)], + [(8, 9), (12, 13)], + [(34, 35), (0, 1)], + [(34, 35), (10, 11)], + [(34, 35), (12, 13)], + [(34, 35), (22, 23)], + [(34, 35), (24, 25)], ], ) @@ -174,7 +174,7 @@ def __init__(self, mapping, pairs) -> None: [11, 24], ], ) -''' #BODY25 annotated +""" #BODY25 annotated pairs_annotated={ "Torso":[1, 8], "Shoulder (right)":[1, 2], @@ -201,7 +201,7 @@ def __init__(self, mapping, pairs) -> None: "Toe (right)":[22, 23], "Heel (right)":[11, 24], } -''' +""" BODY25_FLAT = BodyModel( mapping=[ @@ -257,32 +257,32 @@ def __init__(self, mapping, pairs) -> None: "right_heel_y", ], pairs=[ - [(2,3), (16,17)], - [(2,3), (4,5)], - [(2,3), (10,11)], - [(4,5), (6,7)], - [(6,7), (8,9)], - [(10,11), (12,13)], - [(12,13), (14,15)], - [(16,17), (18,19)], - [(18,19), (20,21)], - [(20,21), (22,23)], - [(16,17), (24,25)], - [(24,25), (26,27)], - [(26,27), (28,29)], - [(2,3), (0,1)], - [(0,1), (30,31)], - [(30,31), (34,35)], - [(0,1), (32,33)], - [(32,33), (36,37)], - [(4,5), (34,35)], - [(10,11), (36,37)], - [(28,29), (38,39)], - [(38,39), (40,41)], - [(28,29), (42,43)], - [(22,23), (44,45)], - [(44,45), (46,47)], - [(22,23), (48,49)], + [(2, 3), (16, 17)], + [(2, 3), (4, 5)], + [(2, 3), (10, 11)], + [(4, 5), (6, 7)], + [(6, 7), (8, 9)], + [(10, 11), (12, 13)], + [(12, 13), (14, 15)], + [(16, 17), (18, 19)], + [(18, 19), (20, 21)], + [(20, 21), (22, 23)], + [(16, 17), (24, 25)], + [(24, 25), (26, 27)], + [(26, 27), (28, 29)], + [(2, 3), (0, 1)], + [(0, 1), (30, 31)], + [(30, 31), (34, 35)], + [(0, 1), (32, 33)], + [(32, 33), (36, 37)], + [(4, 5), (34, 35)], + [(10, 11), (36, 37)], + [(28, 29), (38, 39)], + [(38, 39), (40, 41)], + [(28, 29), (42, 43)], + [(22, 23), (44, 45)], + [(44, 45), (46, 47)], + [(22, 23), (48, 49)], ], ) @@ -291,6 +291,7 @@ def __init__(self, mapping, pairs) -> None: BODY25flat_to_BODY18flat_indices = [0, 1, 32, 33, 30, 31, 36, 37, 34, 35, 10, 11, 4, 5, 12, 13, 6, 7, 14, 15, 8, 9, 24, 25, 18, 19, 26, 27, 20, 21, 28, 29, 22, 23, 2, 3] # fmt: on + def BODY25_to_BODY18(body25_keypoints: np.ndarray): assert body25_keypoints.shape == 25 return body25_keypoints[BODY25_to_BODY18_indices] diff --git a/pose_classification_kit/datasets/data_augmentation.py b/pose_classification_kit/datasets/data_augmentation.py index 9734903..77b6536 100644 --- a/pose_classification_kit/datasets/data_augmentation.py +++ b/pose_classification_kit/datasets/data_augmentation.py @@ -116,12 +116,12 @@ def dataAugmentation( # Remove the points if type(remove_rand_keypoints_nbr) != type(None): if i in list_random_keypoints: - keypoint_x = 0. - keypoint_y = 0. + keypoint_x = 0.0 + keypoint_y = 0.0 if type(remove_specific_keypoints) != type(None): if i in remove_specific_keypoints: - keypoint_x = 0. - keypoint_y = 0. + keypoint_x = 0.0 + keypoint_y = 0.0 # Add additionnal augmentation features entry.append([keypoint_x, keypoint_y]) @@ -137,4 +137,4 @@ def dataAugmentation( new_x = np.array(new_x) new_y = np.array(new_y) - return (new_x,new_y) + return (new_x, new_y) diff --git a/pose_classification_kit/src/keypoints_analysis/body_analysis.py b/pose_classification_kit/src/keypoints_analysis/body_analysis.py index 341d75e..0020a98 100644 --- a/pose_classification_kit/src/keypoints_analysis/body_analysis.py +++ b/pose_classification_kit/src/keypoints_analysis/body_analysis.py @@ -6,7 +6,13 @@ from ..imports.openpose import op from .dynamic_bar_graph_widget import BarGraphWidget from .classifier_selection_widget import ClassifierSelectionWidget -from ...datasets.body_models import BODY18, BODY18_FLAT, BODY25, BODY25_FLAT, BODY25_to_BODY18_indices +from ...datasets.body_models import ( + BODY18, + BODY18_FLAT, + BODY25, + BODY25_FLAT, + BODY25_to_BODY18_indices, +) import numpy as np from matplotlib.backends.backend_qt5agg import FigureCanvas @@ -162,16 +168,18 @@ def updatePredictedClass(self, keypoints: np.ndarray): title = "" if type(keypoints) != type(None): if self.modelClassifier is not None: - + if self.currentBodyModel == BODY25: inputData = keypoints[:2].T elif self.currentBodyModel == BODY25_FLAT: inputData = np.concatenate(keypoints[:2].T, axis=0) elif self.currentBodyModel == BODY18: - inputData = keypoints.T[BODY25_to_BODY18_indices][:,:2] + inputData = keypoints.T[BODY25_to_BODY18_indices][:, :2] elif self.currentBodyModel == BODY18_FLAT: - inputData = np.concatenate(keypoints.T[BODY25_to_BODY18_indices][:,:2], axis=0) - + inputData = np.concatenate( + keypoints.T[BODY25_to_BODY18_indices][:, :2], axis=0 + ) + prediction = self.modelClassifier.predict(np.array([inputData]))[0] self.currentPrediction = self.classOutputs[np.argmax(prediction)] title = self.currentPrediction @@ -187,10 +195,14 @@ def newModelLoaded(self, urlModel: str, modelInfo: dict, bodyID: int): if bodyID == 2: # Check if classifier for body poses (not hands) model = tf.keras.models.load_model(urlModel) nbrClass = model.layers[-1].output_shape[1] - if modelInfo and modelInfo.get('labels') and len(modelInfo.get('labels')) == nbrClass: - classOutputs = modelInfo.get('labels') + if ( + modelInfo + and modelInfo.get("labels") + and len(modelInfo.get("labels")) == nbrClass + ): + classOutputs = modelInfo.get("labels") else: - classOutputs = [str(i) for i in range(1,nbrClass+1)] + classOutputs = [str(i) for i in range(1, nbrClass + 1)] self.setClassifierModel(model, classOutputs) def getCurrentPrediction(self) -> str: diff --git a/pose_classification_kit/src/keypoints_analysis/classifier_selection_widget.py b/pose_classification_kit/src/keypoints_analysis/classifier_selection_widget.py index d65aa73..73e31db 100644 --- a/pose_classification_kit/src/keypoints_analysis/classifier_selection_widget.py +++ b/pose_classification_kit/src/keypoints_analysis/classifier_selection_widget.py @@ -4,6 +4,7 @@ import json + class ClassifierSelectionWidget(QtWidgets.QWidget): # newClassifierModel_Signal: url to load classifier model, model infos from JSON, handID newClassifierModel_Signal = pyqtSignal(str, object, int) @@ -16,7 +17,6 @@ def __init__(self, parent=None, bodyClassification: bool = False): self.modelsPath = MODELS_PATH / ("Body" if bodyClassification else "Hands") self.bodyClassification = bodyClassification - #self.classOutputs = [] self.leftWidget = QtWidgets.QWidget() self.layout = QtWidgets.QGridLayout(self) self.setLayout(self.layout) @@ -50,9 +50,8 @@ def loadModel(self, name: str): """ if name != "None": pathFolder = self.modelsPath / name - print(pathFolder) if pathFolder.is_dir(): - + ModelInfoPath = next(pathFolder.glob("*.json"), None) modelInfo = None if ModelInfoPath: @@ -61,8 +60,6 @@ def loadModel(self, name: str): modelInfo = json.load(f) except: modelInfo = None - #self.classOutputs = first_line.split(",") - #print("Class model loaded:", self.classOutputs) if self.bodyClassification: availableModelPath = next(pathFolder.glob("*.h5"), None) @@ -70,22 +67,16 @@ def loadModel(self, name: str): self.newClassifierModel_Signal.emit( str(availableModelPath), modelInfo, 2 ) - print(str(availableModelPath), "loaded.") else: - print( - "No model found." - ) self.newClassifierModel_Signal.emit("None", {}, 2) - + else: availableModels = list(pathFolder.glob("*_right.h5")) if len(availableModels) > 0: self.newClassifierModel_Signal.emit( str(availableModels[0]), modelInfo, 1 ) - print("Right hand model loaded.") else: - print("No right hand model found.") self.newClassifierModel_Signal.emit("None", {}, 1) availableModels = list(pathFolder.glob("*_left.h5")) @@ -93,22 +84,20 @@ def loadModel(self, name: str): self.newClassifierModel_Signal.emit( str(availableModels[0]), modelInfo, 0 ) - print("Left hand model loaded.") else: - print("No left hand model found.") self.newClassifierModel_Signal.emit("None", {}, 0) else: - print("None") -# self.modelRight = None -# self.modelLeft = None -# self.classOutputs = [] self.newClassifierModel_Signal.emit("None", {}, -1) def getAvailableClassifiers(self): listOut = ["None"] # Get all directory that contains an h5 file. - listOut += [x.stem for x in self.modelsPath.glob('*') if x.is_dir() and next(x.glob('*.h5'), None)] + listOut += [ + x.stem + for x in self.modelsPath.glob("*") + if x.is_dir() and next(x.glob("*.h5"), None) + ] return listOut def updateClassifier(self): diff --git a/pose_classification_kit/src/keypoints_analysis/hand_analysis.py b/pose_classification_kit/src/keypoints_analysis/hand_analysis.py index 8709a09..ebf219b 100644 --- a/pose_classification_kit/src/keypoints_analysis/hand_analysis.py +++ b/pose_classification_kit/src/keypoints_analysis/hand_analysis.py @@ -158,12 +158,16 @@ def newModelLoaded(self, urlModel: str, modelInfo: list, handID: int): if handID == self.handID: model = tf.keras.models.load_model(urlModel) nbrClass = model.layers[-1].output_shape[1] - if modelInfo and modelInfo.get('labels') and len(modelInfo.get('labels')) == nbrClass: - classOutputs = modelInfo.get('labels') + if ( + modelInfo + and modelInfo.get("labels") + and len(modelInfo.get("labels")) == nbrClass + ): + classOutputs = modelInfo.get("labels") else: - classOutputs = [str(i) for i in range(1,nbrClass+1)] + classOutputs = [str(i) for i in range(1, nbrClass + 1)] self.setClassifierModel(model, classOutputs) - + def getCurrentPrediction(self) -> str: return self.currentPrediction diff --git a/pose_classification_kit/src/video_analysis/video_manager.py b/pose_classification_kit/src/video_analysis/video_manager.py index 80b9190..4389fca 100644 --- a/pose_classification_kit/src/video_analysis/video_manager.py +++ b/pose_classification_kit/src/video_analysis/video_manager.py @@ -74,31 +74,30 @@ def qImageToMat(self, incomingImage): mat = cv2.imread(self.tmpUrl) return mat - def qImageToMat_alt(self,incomingImage): - ''' Converts a QImage into an opencv MAT format ''' + def qImageToMat_alt(self, incomingImage): + """ Converts a QImage into an opencv MAT format """ # Convert to 32-bit RGBA with solid opaque alpha # and get the pointer numpy will want. - # + # # Cautions: # 1. I think I remember reading that PyQt5 only has # constBits() and PySide2 only has bits(), so you may # need to do something like `if hasattr(...)` for # portability. - # - # 2. Format_RGBX8888 is native-endian for your + # + # 2. Format_RGBX8888 is native-endian for your # platform and I suspect this code, as-is, # would break on a big-endian system. im_in = incomingImage.convertToFormat(QtGui.QImage.Format_RGBX8888) ptr = im_in.constBits() ptr.setsize(im_in.byteCount()) - # Convert the image into a numpy array in the + # Convert the image into a numpy array in the # format PyOpenCV expects to operate on, explicitly # copying to avoid potential lifetime bugs when it # hasn't yet proven a performance issue for my uses. - cv_im_in = np.array(ptr, copy=True).reshape( - im_in.height(), im_in.width(), 4) + cv_im_in = np.array(ptr, copy=True).reshape(im_in.height(), im_in.width(), 4) cv_im_in = cv2.cvtColor(cv_im_in, cv2.COLOR_BGRA2RGB) return cv_im_in diff --git a/pyproject.toml b/pyproject.toml index 56afd6d..6e9749f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pose-classification-kit" -version = "1.1.4" +version = "1.1.5" description = "From pose estimation to pose classification - Creation of datasets & real-time visualization" authors = ["ArthurFDLR "] keywords = ["pose-classification", "OpenPose", "pose-estimation", "machine-learning", "deep-learning", "keypoints", "keypoints-detection", "gesture"] @@ -30,7 +30,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.6.2, <4.0" tensorflow = "*" -numpy = "*" #">=1.19.0,<1.19.4" +numpy = "~1.19.2" # tensorflow requirement pandas = "^1.1.5" opencv-python = {version = "^4.4.0", optional = true} matplotlib = {version = "^3.3.2", optional = true}