From 27966900703f79833b63d78d94ca5f9b652b65f9 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:43:48 +0900 Subject: [PATCH 01/25] Feat: add absoulte path util --- VisageSnap/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/VisageSnap/utils.py b/VisageSnap/utils.py index 53a44a7..12af489 100644 --- a/VisageSnap/utils.py +++ b/VisageSnap/utils.py @@ -10,4 +10,8 @@ def gen(target: list[any]) -> any: """ assert isinstance(target, list | np.ndarray), "target must be a list or numpy.ndarray." for i in target: - yield i \ No newline at end of file + yield i + +def absp(value: str) -> str: + return os.path.join(os.getcwd(), value) + From 64ef95a6b3d2457d9c7be894809b41e6a78b4c0d Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:44:33 +0900 Subject: [PATCH 02/25] Feat: replace isImage method to utils --- VisageSnap/utils.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/VisageSnap/utils.py b/VisageSnap/utils.py index 12af489..f950816 100644 --- a/VisageSnap/utils.py +++ b/VisageSnap/utils.py @@ -15,3 +15,23 @@ def gen(target: list[any]) -> any: def absp(value: str) -> str: return os.path.join(os.getcwd(), value) +def isimage(filename: str) -> bool: + """ + This function checks if the file is an image file. + + Parameters + ---------- + filename (str) : target filename. + """ + assert isinstance(filename, str), "filename must be a string." + + list = [ + ".jpg", + ".png", + ".jpeg" + ] + + for i in list: + if filename.endswith(i): + return True + return False \ No newline at end of file From 0ce92e0810e0e97029b226e13cfbc7400ad3106d Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:45:17 +0900 Subject: [PATCH 03/25] Feat: add singleton class decorator --- VisageSnap/main.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index bcc159c..8baa8e4 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -7,26 +7,16 @@ import pickle from .classes import * from .utils import * + +def singleton(class_): + instances = {} + def get_instance(*args, **kwargs): + if class_ not in instances: + instances[class_] = class_(*args, **kwargs) + return instances[class_] -# Make a class to semi-supervised the face recognition -class Core(): - def __init__(self): - """ - VisageSnap Core Class - --------------------- - """ - _default_dir = os.getcwd() - self.faces: list[Face] = [] - - # Directory - self.unlabeled_dir = os.path.join(_default_dir, "unlabeled") - self.labeled_dir = os.path.join(_default_dir, "labeled") - - self.model_dir = os.path.join(_default_dir, "model", "face_model.pkl") - - self.predict_dir = os.path.join(_default_dir, "predict") - + return get_instance self.label: dict = {} self.threshold = 0.42 From 7ca29f70d3c86542f24c9094808c21693b741656 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:45:59 +0900 Subject: [PATCH 04/25] Feat: add FaceProcessor, base class --- VisageSnap/main.py | 178 ++++++++++++++++----------------------------- 1 file changed, 61 insertions(+), 117 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 8baa8e4..607c8ff 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -17,58 +17,74 @@ def get_instance(*args, **kwargs): return instances[class_] return get_instance + +@singleton +class FaceProcessor: + def __init__(self): + self.__directory = { + "labeled": absp("labeled"), + "unlabeled": absp("unlabeled"), + "model": absp("model"), + "predict": absp("predict") + } + self.faces: list[Face] = [] self.label: dict = {} - - self.threshold = 0.42 - - self.model = self._load_model() - - @staticmethod - def _isImage(filename: str) -> bool: - """ - This function checks if the file is an image file. - - Parameters - ---------- - filename (str) : target filename. + + @property + def dir(self) -> dict: """ - assert isinstance(filename, str), "filename must be a string." - - list = [ - ".jpg", - ".png", - ".jpeg" - ] - - for i in list: - if filename.endswith(i): - return True - return False + This function returns the directory. - def get_faceObject(self, target: str, value: str) -> Face: - """ - This function returns the face object with the given label. + Returns + ------- + dict (dict) : directory dictionary. - Parameters - ---------- - target: - - "From.LABEL" : label of the face object. (name of the person) - - "From.FILENAME" : filename of the face object. + Example + ------- + dict = { + "labeled": "labeled", + "unlabeled": "unlabeled", + "model": "model", + "predict": "predict" + } + - labeled : directory of the labeled data + - unlabeled : directory of the unlabeled data + - model : directory of the model + - predict : directory of the predict data - value (str) : value of the target. + Default + ------- + labeled : "labeled" + unlabeled : "unlabeled" + model : "model" + predict : "predict" """ - assert isinstance(target, str), "target must be 'From.LABEL' or 'From.FILENAME'." - assert isinstance(value, str), "value must be a string." - - for face in self.gen_faces(): - if target == "Label": - if face.label == value: - return face - elif target == "Filename": - if value in face.filenames: - return face - return None + return self.__directory + + @dir.setter + def dir(self, dicto: dict) -> None: + def _set_dir(key: str, value: str): + # Check if directory exists + if os.path.isdir(value) == False: + raise("The directory does not exist.") + + # Check if value is absoulte path or relative path. if relative, convert to absolute path. + value = absp(value) if os.path.isabs(value) is False else value + + self.__directory[key] = value + for key, value in dicto.items(): + if key == "labeled": + _set_dir(key, value) + elif key == "unlabeled": + _set_dir(key, value) + elif key == "model": + _set_dir(key, value) + elif key == "predict": + _set_dir(key, value) + else: + raise("The key is not valid.") + def gen_faces(self) -> list[Face]: """ This function returns the face list. @@ -77,78 +93,6 @@ def gen_faces(self) -> list[Face]: for face in self.faces: yield face - - def _load_labeled(self) -> None: # 미리 주어지는 데이터는 한 사진에 한 사람만 있어야 한다. - """ - This function loads the labeled data from the labeled directory. - """ - for filename in gen(os.listdir(self.labeled_dir)): - if self._isImage(filename): - label = (filename.split(".")[0]).split("-")[0] # 파일 형식은 이름-번호.jpg - image = face_recognition.load_image_file(os.path.join(self.labeled_dir, filename)) - encodings = face_recognition.face_encodings(image) - encoding = encodings[0] - - # 만약 두개의 얼굴이 같은 사진에 있다면 - if len(encodings) > 1: - # 두개 이상의 얼굴... 시마이 - continue - - # 만약 같은 얼굴이 있다면 - FACE_FOUND = False - for i, face in enumerate(self.faces): #얼굴 검색 - if face.label == label: # 같은 얼굴이라면 - for faceEncoding in face.encodings: - # 인코딩 같은게 있는지 확인 - if np.array_equal(faceEncoding, encoding): - # 같은 게 있음 - continue - - # 인코딩이 다르다면 얼굴에 추가 - self.faces[i].encodings.append(encoding) - self.faces[i].filenames.append(filename) - FACE_FOUND = True - - if not FACE_FOUND: - self.faces.append(Face(label, [encoding], [filename])) - - - def _load_unlabeled(self) -> None: - """ - This function loads the unlabeled data from the unlabeled directory. - """ - for filename in gen(os.listdir(self.unlabeled_dir)): - if self._isImage(filename): - image = face_recognition.load_image_file(os.path.join(self.unlabeled_dir, filename)) - encodings = face_recognition.face_encodings(image) - - if len(encodings) == 0: - # 얼굴 감지 안됨 - continue - - for encoding in encodings: - self.faces.append(Face("unknown", encoding, [filename])) - - - def _load_model(self) -> LabelPropagation: - try: - with open(self.model_dir, "rb") as f: - self.model, self.faces = pickle.load(f) - print("Model loaded.") - return self.model - except: # 새 모델 만들기 - self.model = LabelPropagation() - self.faces = [] # 초기화 - return self.model - - def _save_model(self) -> None: - if not os.path.exists(os.path.join(os.getcwd(), "model")): - os.mkdir(os.path.join(os.getcwd(), "model")) - - data = (self.model, self.faces) - with open(self.model_dir, "wb") as f: - pickle.dump(data, f) - def convert_labelType(self, value, to: str) -> any: """ This function converts the label type. (numberLabel -> nameLabel, nameLabel -> numberLabel) From ca3e0877360a49755b7e9d01805afc148d627e2a Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:46:40 +0900 Subject: [PATCH 05/25] Feat: add ModelManager class --- VisageSnap/main.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 607c8ff..94435c1 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -146,8 +146,29 @@ def set_label(self, person: any) -> None: elif type(person) == list: for i in range(len(person)): self.label[person[i]] = i + +class ModelManager: + def __init__(self): + self.fp = FaceProcessor() + + def load(self) -> LabelPropagation: + try: + with open(self.fp.dir["model"], "rb") as f: + self.model, self.fp.faces = pickle.load(f) + print("Model loaded.") + return self.model + except: # 새 모델 만들기 + self.model = LabelPropagation() + self.fp.faces = [] # 초기화 + return self.model + + def save(self) -> None: + if not os.path.exists(absp("model")): + os.mkdir(absp("model")) - def set_directory(self, dicto: dict) -> None: + data = (self.model, self.fp.faces) + with open(self.fp.dir["model"], "wb") as f: + pickle.dump(data, f) """ This function sets the directory. From b1ebd693da84ca2d3161663bb4a4a9b6ed5209c6 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:46:59 +0900 Subject: [PATCH 06/25] Feat: add ImageLoader class --- VisageSnap/main.py | 87 ++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 94435c1..47e4b33 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -169,50 +169,63 @@ def save(self) -> None: data = (self.model, self.fp.faces) with open(self.fp.dir["model"], "wb") as f: pickle.dump(data, f) + +@singleton +class ImageLoader(FaceProcessor): + def __init__(self): + super().__init__() + + def __load_labeled(self) -> None: # 미리 주어지는 데이터는 한 사진에 한 사람만 있어야 한다. + """ + This function loads the labeled data from the labeled directory. """ - This function sets the directory. + for filename in gen(os.listdir(self.dir["labeled"])): + if isimage(filename): + label = (filename.split(".")[0]).split("-")[0] # 파일 형식은 이름-번호.jpg + image = face_recognition.load_image_file(os.path.join(self.dir["labeled"], filename)) + encodings = face_recognition.face_encodings(image) + encoding = encodings[0] + + # 만약 두개의 얼굴이 같은 사진에 있다면 + if len(encodings) > 1: + # 두개 이상의 얼굴... 시마이 + continue - Parameters - ---------- - dict (dict) : directory dictionary. + # 만약 같은 얼굴이 있다면 + FACE_FOUND = False + for i, face in enumerate(self.gen_faces): #얼굴 검색 + if face.label == label: # 같은 얼굴이라면 + for faceEncoding in face.encodings: + # 인코딩 같은게 있는지 확인 + if np.array_equal(faceEncoding, encoding): + # 같은 게 있음 + continue - Example - ------- - dict = { - "labeled": "labeled", - "unlabeled": "unlabeled", - "model": "model", - "predict": "predict" - } - - labeled : directory of the labeled data - - unlabeled : directory of the unlabeled data - - model : directory of the model - - predict : directory of the predict data + # 인코딩이 다르다면 얼굴에 추가 + self.faces[i].encodings.append(encoding) + self.faces[i].filenames.append(filename) + FACE_FOUND = True - Default - ------- - labeled : "labeled" - unlabeled : "unlabeled" - model : "model" - predict : "predict" + if not FACE_FOUND: + self.faces.append(Face(label, [encoding], [filename])) + + + def __load_unlabeled(self) -> None: + """ + This function loads the unlabeled data from the unlabeled directory. """ - assert isinstance(dicto, dict), "parameter must be dictionary." + for filename in gen(os.listdir(self.dir["unlabeled"])): + if self._isImage(filename): + image = face_recognition.load_image_file(os.path.join(self.dir["unlabeled"], filename)) + encodings = face_recognition.face_encodings(image) - def _set_dir(key: str, value: str) -> None: - if value[:1] == "/": - dicto[key] = value - else: - dicto[key] = os.path.join(os.getcwd(), value) + if len(encodings) == 0: + # 얼굴 감지 안됨 + continue + + for encoding in encodings: + self.faces.append(Face("unknown", encoding, [filename])) - for key, value in dicto.items(): - if key == "labeled": - _set_dir(key, value) - elif key == "unlabeled": - _set_dir(key, value) - elif key == "model": - _set_dir(key, value) - elif key == "predict": - _set_dir(key, value) From 9eed4195bf7cd8dbaa8a9568d2189d0633863184 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:47:17 +0900 Subject: [PATCH 07/25] Feat: add Trainer class --- VisageSnap/main.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 47e4b33..f6b6662 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -227,15 +227,20 @@ def __load_unlabeled(self) -> None: self.faces.append(Face("unknown", encoding, [filename])) +class Trainer(FaceProcessor): + def __init__(self, modelManager: ModelManager = None): + super().__init__() + + self.modelManager = modelManager + self.il = ImageLoader() - - def _train(self, labeled: bool) -> None: + def __train(self, labeled: bool) -> None: assert isinstance(labeled, bool), "parameter must be boolean." if labeled: - self._load_labeled() + self.il.__load_labeled() else: - self._load_unlabeled() + self.il.__load_unlabeled() t_names = [] t_encodings =[] @@ -251,15 +256,14 @@ def _train(self, labeled: bool) -> None: t_encodings = np.array(t_encodings) t_names = np.array(t_names) - self.model.fit(t_encodings, t_names) + self.modelManager.model.fit(t_encodings, t_names) self._save_model() - def train_labeled_data(self) -> None: - self._train(As.LABELED) + self.__train(As.LABELED) def train_unlabeled_data(self) -> None: - self._train(As.UNLABELED) + self.__train(As.UNLABELED) @staticmethod From 0dbc73a3a475fbb026c00089a0e553dce5155860 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:47:39 +0900 Subject: [PATCH 08/25] Feat: add Predictor class --- VisageSnap/main.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index f6b6662..5d07faf 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -265,9 +265,15 @@ def train_labeled_data(self) -> None: def train_unlabeled_data(self) -> None: self.__train(As.UNLABELED) +class Predictor(FaceProcessor): + def __init__(self, modelManager: ModelManager = None): + super().__init__() + + self.modelManager = modelManager + self.threshold = 0.48 @staticmethod - def _get_average(face: Face) -> np.array: + def __get_average(face: Face) -> np.array: """ This function returns the average of the encodings. @@ -279,7 +285,7 @@ def _get_average(face: Face) -> np.array: return np.average(face.encodings, axis=0) @staticmethod - def _get_distance(encoding1: np.ndarray, encoding2: np.ndarray) -> float: + def __get_distance(encoding1: np.ndarray, encoding2: np.ndarray) -> float: """ This function returns the distance between two encodings. @@ -293,7 +299,7 @@ def _get_distance(encoding1: np.ndarray, encoding2: np.ndarray) -> float: return np.linalg.norm(encoding1 - encoding2) - def _isNotUnknown(self, encoding) -> bool: + def __isNotUnknown(self, encoding) -> bool: """ This function checks whether the encoding is unknown. @@ -306,8 +312,8 @@ def _isNotUnknown(self, encoding) -> bool: min_distance = 1 for face in self.gen_faces(): print(face.label) - average = self._get_average(face) # 저장된 얼굴 평균 구하고 - distance = self._get_distance(encoding, average) # 타겟과의 거리를 구한다 + average = self.__get_average(face) # 저장된 얼굴 평균 구하고 + distance = self.__get_distance(encoding, average) # 타겟과의 거리를 구한다 if distance < min_distance: min_distance = distance @@ -315,7 +321,7 @@ def _isNotUnknown(self, encoding) -> bool: return True # 모르는 사람이 아니다 return False # 모르는 사람이다 - def predict(self, image: np.ndarray) -> list: + def __predict(self, image: np.ndarray) -> list: assert isinstance(image, np.ndarray), "parameter must be numpy array." target_encodings = face_recognition.face_encodings(image) @@ -324,8 +330,8 @@ def predict(self, image: np.ndarray) -> list: result = [] for target_encoding in target_encodings: - if self._isNotUnknown(target_encoding): # 모르는 사람이 아니면 - result.append(self.model.predict([target_encoding])[0]) + if self.__isNotUnknown(target_encoding): # 모르는 사람이 아니면 + result.append(self.modelManager.model.predict([target_encoding])[0]) else: result.append(-1) @@ -335,11 +341,11 @@ def predict(self, image: np.ndarray) -> list: def predict_all(self) -> dict: result = {} - for filename in os.listdir(self.predict_dir): - if self._isImage(filename) == False: + for filename in os.listdir(self.dir["predict"]): + if isimage(filename) == False: return - image = face_recognition.load_image_file(os.path.join(self.predict_dir, filename)) + image = face_recognition.load_image_file(os.path.join(self.dir["predict"], filename)) prediction = self.predict(image) if prediction == None: From 87ffd786bec6dcbf64f33fd08d77e7ce904578b9 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:48:06 +0900 Subject: [PATCH 09/25] Refactor: refactoring main classes --- VisageSnap/main.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 5d07faf..a03f8f7 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -357,4 +357,29 @@ def predict_all(self) -> dict: result[filename] = [] for p in prediction: result[filename].append(self.convert_labelType(p, To.NAME)) - return result \ No newline at end of file + return result + +class Core(FaceProcessor): + def __init__(self, modelManager: ModelManager = None): + super().__init__() + + if modelManager is None: + self.modelManager = ModelManager() # create New ModelManager Object + else: + self.modelManager = modelManager # or use the given ModelManager Object + + + self.trainer = Trainer(modelManager) + self.predictor = Predictor(modelManager) + + def train_labeled_data(self) -> None: + self.trainer.train_labeled_data() + + def train_unlabeled_data(self) -> None: + self.trainer.train_unlabeled_data() + + def predict(self, image: np.ndarray) -> list: + return self.predictor.predict(image) + + def predict_all(self) -> dict: + return self.predictor.predict_all() \ No newline at end of file From 5a9d1debdfecf6c87580215a392459f030185604 Mon Sep 17 00:00:00 2001 From: asheswook Date: Sat, 27 May 2023 20:48:45 +0900 Subject: [PATCH 10/25] Chore: import os module for isimage util --- VisageSnap/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/VisageSnap/utils.py b/VisageSnap/utils.py index f950816..6edb7e2 100644 --- a/VisageSnap/utils.py +++ b/VisageSnap/utils.py @@ -1,4 +1,5 @@ import numpy as np +import os def gen(target: list[any]) -> any: """ From f32409f74e3b6a6fa6141aa9ac34b12584a14fab Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:39:32 +0900 Subject: [PATCH 11/25] Chore: import type hinting module --- VisageSnap/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index a03f8f7..6b4e4c6 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -5,6 +5,7 @@ from sklearn.semi_supervised import LabelPropagation from sklearn.exceptions import NotFittedError import pickle +from typing import Generator, Union from .classes import * from .utils import * From 8487d5973baed2590d90cd73fcf8644876a5fd55 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:45:03 +0900 Subject: [PATCH 12/25] Feat: add global states --- VisageSnap/classes.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/VisageSnap/classes.py b/VisageSnap/classes.py index d2cc2bb..6eb2e34 100644 --- a/VisageSnap/classes.py +++ b/VisageSnap/classes.py @@ -1,5 +1,6 @@ from dataclasses import dataclass import numpy as np +from sklearn.semi_supervised import LabelPropagation @dataclass class Face(): @@ -7,6 +8,19 @@ class Face(): encodings: np.ndarray filenames: list +@dataclass +class GlobalState: + faces: list[Face] + label: dict + model: LabelPropagation + +@dataclass +class Directory: + labeled: str + unlabeled: str + model: str + predict: str + @dataclass class From(): LABEL = "Label" From 2ebc6d224894d6da967fea5422fe9a7d1294b21a Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:47:14 +0900 Subject: [PATCH 13/25] Refactor: faceprocessor class as parent --- VisageSnap/main.py | 104 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 6b4e4c6..1072a01 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -8,29 +8,93 @@ from typing import Generator, Union from .classes import * from .utils import * - -def singleton(class_): - instances = {} - - def get_instance(*args, **kwargs): - if class_ not in instances: - instances[class_] = class_(*args, **kwargs) - return instances[class_] - return get_instance - -@singleton class FaceProcessor: - def __init__(self): - self.__directory = { - "labeled": absp("labeled"), - "unlabeled": absp("unlabeled"), - "model": absp("model"), - "predict": absp("predict") + def __init__(self, globalState: GlobalState): + self.__state = globalState + + def gen_faces(self) -> Generator[Face, None, None]: + for face in self.__state.faces: + yield face + + def convert_labelType(self, value: Union[str, int], to: str) -> any: + """ + This function converts the label type. (numberLabel -> nameLabel, nameLabel -> numberLabel) + + Parameters + ---------- + value (str or int) : target value. + to (str) : + - "To.NAME" : convert to name label. + - "To.NUMBER" : convert to number label. + + Returns + ------- + str or int : converted value. (if To.NAME, return str, if To.NUMBER, return int) + """ + if to == "Name": + for name, number in self.__state.label.items(): + if number == value: + return name + elif to == "Number": + return self.__state.label.get(value, -1) + return None + + def set_label(self, person: Union[list, dict]) -> None: + """ + This function sets the label dictionary. + + Parameters + ---------- + person (list or dict) : label list or dictionary. + + Example + ------- + person = ["name1", "name2", "name3", ...] + + OR + + person = { + "name1": 0, + "name2": 1, + "name3": 2, + ... } - self.faces: list[Face] = [] - self.label: dict = {} - + + - name1, name2, name3, ... : name of the person + - 0, 1, 2, ... : number label (MUST NOT BE -1) + """ + + if type(person) == dict: + self.__state.label = person + elif type(person) == list: + for i in range(len(person)): + self.__state.label[person[i]] = i + + def get_faceObject(self, target: str, value: str) -> Face: + """ + This function returns the face object with the given label. + + Parameters + ---------- + target: + - "From.LABEL" : label of the face object. (name of the person) + - "From.FILENAME" : filename of the face object. + + value (str) : value of the target. + """ + assert isinstance(target, str), "target must be 'From.LABEL' or 'From.FILENAME'." + assert isinstance(value, str), "value must be a string." + + for face in self.gen_faces(): + if target == "Label": + if face.label == value: + return face + elif target == "Filename": + if value in face.filenames: + return face + return None + @property def dir(self) -> dict: """ From c07938a8b31acf7ce9775271820793425ab6319a Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:48:21 +0900 Subject: [PATCH 14/25] Refactor: sync dirmanager with globalstate --- VisageSnap/main.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 1072a01..c0e7ad4 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -95,6 +95,10 @@ def get_faceObject(self, target: str, value: str) -> Face: return face return None +class DirectoryManager: + def __init__(self, directory: Directory): + self.__directory = directory + @property def dir(self) -> dict: """ @@ -136,7 +140,14 @@ def _set_dir(key: str, value: str): # Check if value is absoulte path or relative path. if relative, convert to absolute path. value = absp(value) if os.path.isabs(value) is False else value - self.__directory[key] = value + if key == "labeled": + self.__directory.labeled = value + elif key == "unlabeled": + self.__directory.unlabeled = value + elif key == "model": + self.__directory.model = value + elif key == "predict": + self.__directory.predict = value for key, value in dicto.items(): if key == "labeled": From 57c38b79e8c478e84de1d35311a33266b081975d Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:48:50 +0900 Subject: [PATCH 15/25] Refactor: modelhandler --- VisageSnap/main.py | 70 +++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 47 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index c0e7ad4..003fe3b 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -161,61 +161,37 @@ def _set_dir(key: str, value: str): else: raise("The key is not valid.") - def gen_faces(self) -> list[Face]: - """ - This function returns the face list. - """ - result = [] - for face in self.faces: - yield face - - def convert_labelType(self, value, to: str) -> any: - """ - This function converts the label type. (numberLabel -> nameLabel, nameLabel -> numberLabel) - Parameters - ---------- - value (str or int) : target value. - to (str) : - - "To.NAME" : convert to name label. - - "To.NUMBER" : convert to number label. +class ModelHandler: + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + self.__state = globalState + self.__directory = directory + self.__state.model = self.load() - Returns - ------- - str or int : converted value. (if To.NAME, return str, if To.NUMBER, return int) - """ - if to == "Name": - for name, number in self.label.items(): - if number == value: - return name - elif to == "Number": - return self.label.get(value, -1) - return None + def load(self) -> LabelPropagation: + try: + model_path = os.path.join(self.__directory.model, "face_model.pkl") - def set_label(self, person: any) -> None: - """ - This function sets the label dictionary. + with open(model_path, "rb") as f: + self.__state.model, self.__state.faces = pickle.load(f) + print("Model loaded.") - Parameters - ---------- - person (list or dict) : label list or dictionary. + except: + print("Model not found. Creating new model...") + self.__state.model = LabelPropagation() + self.__state.faces = [] # 초기화 - Example - ------- - person = ["name1", "name2", "name3", ...] + return self.__state.model + + def save(self) -> None: + model_path = os.path.join(self.__directory.model, "face_model.pkl") - OR + # Create directory if not exists + os.mkdir(self.__directory.model) if not os.path.exists(self.__directory.model) else None - person = { - "name1": 0, - "name2": 1, - "name3": 2, - ... - } + with open(model_path, "wb") as f: + pickle.dump((self.__state.model, self.__state.faces), f) # Save as tuple - - name1, name2, name3, ... : name of the person - - 0, 1, 2, ... : number label (MUST NOT BE -1) - """ if type(person) == dict: self.label = person From a770947a52aff7382ccf5c6e8ecdfb4335198673 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:49:16 +0900 Subject: [PATCH 16/25] Refactor: ImageLoader --- VisageSnap/main.py | 90 ++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 60 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 003fe3b..2ebeca7 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -193,92 +193,62 @@ def save(self) -> None: pickle.dump((self.__state.model, self.__state.faces), f) # Save as tuple - if type(person) == dict: - self.label = person - elif type(person) == list: - for i in range(len(person)): - self.label[person[i]] = i - -class ModelManager: - def __init__(self): - self.fp = FaceProcessor() - - def load(self) -> LabelPropagation: - try: - with open(self.fp.dir["model"], "rb") as f: - self.model, self.fp.faces = pickle.load(f) - print("Model loaded.") - return self.model - except: # 새 모델 만들기 - self.model = LabelPropagation() - self.fp.faces = [] # 초기화 - return self.model - - def save(self) -> None: - if not os.path.exists(absp("model")): - os.mkdir(absp("model")) - - data = (self.model, self.fp.faces) - with open(self.fp.dir["model"], "wb") as f: - pickle.dump(data, f) +class ImageLoader: + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + self.__state = globalState + self.__directory = directory -@singleton -class ImageLoader(FaceProcessor): - def __init__(self): - super().__init__() - - def __load_labeled(self) -> None: # 미리 주어지는 데이터는 한 사진에 한 사람만 있어야 한다. + def load_labeled(self) -> None: """ This function loads the labeled data from the labeled directory. + Only allows one face per image. """ - for filename in gen(os.listdir(self.dir["labeled"])): + for filename in gen(os.listdir(self.__directory.labeled)): if isimage(filename): - label = (filename.split(".")[0]).split("-")[0] # 파일 형식은 이름-번호.jpg - image = face_recognition.load_image_file(os.path.join(self.dir["labeled"], filename)) + label = (filename.split(".")[0]).split("-")[0] # 파일 형식은 이름-번호.jpg임. 이름만 추출 + image = face_recognition.load_image_file(os.path.join(self.__directory.labeled, filename)) encodings = face_recognition.face_encodings(image) encoding = encodings[0] - # 만약 두개의 얼굴이 같은 사진에 있다면 + # If there are more than one face, skip. if len(encodings) > 1: - # 두개 이상의 얼굴... 시마이 continue - # 만약 같은 얼굴이 있다면 + # Check if the face is already in the state. FACE_FOUND = False - for i, face in enumerate(self.gen_faces): #얼굴 검색 - if face.label == label: # 같은 얼굴이라면 - for faceEncoding in face.encodings: - # 인코딩 같은게 있는지 확인 - if np.array_equal(faceEncoding, encoding): - # 같은 게 있음 + for i, face in enumerate(self.__state.faces): # Search for the face in the state + + if face.label == label: # If the face label is already in the state + for old_encoding in face.encodings: + + # Check if the encoding is already in the face. + if np.array_equal(old_encoding, encoding): continue - # 인코딩이 다르다면 얼굴에 추가 - self.faces[i].encodings.append(encoding) - self.faces[i].filenames.append(filename) + # If the encoding is not in the face, append it. + self.__state.faces[i].encodings.append(encoding) + self.__state.faces[i].filenames.append(filename) FACE_FOUND = True - if not FACE_FOUND: - self.faces.append(Face(label, [encoding], [filename])) - + if not FACE_FOUND: # If the face is not in the state, create new face. + self.__state.faces.append(Face(label, [encoding], [filename])) - def __load_unlabeled(self) -> None: + def load_unlabeled(self) -> None: """ This function loads the unlabeled data from the unlabeled directory. """ - for filename in gen(os.listdir(self.dir["unlabeled"])): - if self._isImage(filename): - image = face_recognition.load_image_file(os.path.join(self.dir["unlabeled"], filename)) + for filename in gen(os.listdir(self.__directory.unlabeled)): + if isimage(filename): + image = face_recognition.load_image_file(os.path.join(self.__directory.unlabeled, filename)) encodings = face_recognition.face_encodings(image) - if len(encodings) == 0: - # 얼굴 감지 안됨 + if len(encodings) == 0: # If there is no face, skip. continue for encoding in encodings: - self.faces.append(Face("unknown", encoding, [filename])) - + self.__state.faces.append(Face("unknown", encoding, [filename])) + class Trainer(FaceProcessor): def __init__(self, modelManager: ModelManager = None): super().__init__() From cd7de7c5cdae03de0974fedbeab381e4426892d7 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:49:41 +0900 Subject: [PATCH 17/25] Refactor: sync trainer with globalstates --- VisageSnap/main.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 2ebeca7..748e42a 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -250,19 +250,21 @@ def load_unlabeled(self) -> None: class Trainer(FaceProcessor): - def __init__(self, modelManager: ModelManager = None): - super().__init__() - - self.modelManager = modelManager - self.il = ImageLoader() + def __init__(self, globalState: GlobalState = None, directory: Directory = None, imageLoader: ImageLoader = None, modelHandler: ModelHandler = None): + super().__init__(globalState) + + self.__state = globalState + self.__directory = directory + self.imageLoader = ImageLoader(self.__state, self.__directory) if imageLoader is None else imageLoader + self.modelHandler = ModelHandler(self.__state, self.__directory) if modelHandler is None else modelHandler def __train(self, labeled: bool) -> None: assert isinstance(labeled, bool), "parameter must be boolean." if labeled: - self.il.__load_labeled() + self.imageLoader.load_labeled() else: - self.il.__load_unlabeled() + self.imageLoader.load_unlabeled() t_names = [] t_encodings =[] @@ -278,8 +280,8 @@ def __train(self, labeled: bool) -> None: t_encodings = np.array(t_encodings) t_names = np.array(t_names) - self.modelManager.model.fit(t_encodings, t_names) - self._save_model() + self.__state.model.fit(t_encodings, t_names) + self.modelHandler.save() def train_labeled_data(self) -> None: self.__train(As.LABELED) From 84a57f4ed247c7afce58eaefab28b64ce85c4ee6 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:50:18 +0900 Subject: [PATCH 18/25] Refactor: sync Predictor with globalstates --- VisageSnap/main.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 748e42a..9159d25 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -290,10 +290,11 @@ def train_unlabeled_data(self) -> None: self.__train(As.UNLABELED) class Predictor(FaceProcessor): - def __init__(self, modelManager: ModelManager = None): - super().__init__() + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + super().__init__(globalState) - self.modelManager = modelManager + self.__state = globalState + self.__directory = directory self.threshold = 0.48 @staticmethod @@ -335,7 +336,6 @@ def __isNotUnknown(self, encoding) -> bool: min_distance = 1 for face in self.gen_faces(): - print(face.label) average = self.__get_average(face) # 저장된 얼굴 평균 구하고 distance = self.__get_distance(encoding, average) # 타겟과의 거리를 구한다 if distance < min_distance: @@ -355,22 +355,20 @@ def __predict(self, image: np.ndarray) -> list: result = [] for target_encoding in target_encodings: if self.__isNotUnknown(target_encoding): # 모르는 사람이 아니면 - result.append(self.modelManager.model.predict([target_encoding])[0]) + result.append(self.__state.model.predict([target_encoding])[0]) else: result.append(-1) return result - - def predict_all(self) -> dict: result = {} - for filename in os.listdir(self.dir["predict"]): + for filename in os.listdir(self.__directory.predict): if isimage(filename) == False: return - image = face_recognition.load_image_file(os.path.join(self.dir["predict"], filename)) - prediction = self.predict(image) + image = face_recognition.load_image_file(os.path.join(self.__directory.predict, filename)) + prediction = self.__predict(image) if prediction == None: raise("There is no face in the image.") From 515b63bd497a7e695609eb20a7da790ce7785d23 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 00:50:34 +0900 Subject: [PATCH 19/25] Refactor: core class --- VisageSnap/main.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 9159d25..9d84f3c 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -382,17 +382,20 @@ def predict_all(self) -> dict: return result class Core(FaceProcessor): - def __init__(self, modelManager: ModelManager = None): - super().__init__() - - if modelManager is None: - self.modelManager = ModelManager() # create New ModelManager Object - else: - self.modelManager = modelManager # or use the given ModelManager Object + """ + VisageSnap Core Class + --------------------- + """ + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + self.__state = GlobalState([], {}, None) if globalState is None else globalState + self.__directory = Directory("labeled", "unlabeled", "model", "predict") if directory is None else directory + super().__init__(self.__state) - self.trainer = Trainer(modelManager) - self.predictor = Predictor(modelManager) + self.modelHandler = ModelHandler(self.__state, self.__directory) + self.imageLoader = ImageLoader(self.__state, self.__directory) + self.trainer = Trainer(self.__state, self.__directory, self.imageLoader, self.modelHandler) + self.predictor = Predictor(self.__state, self.__directory) def train_labeled_data(self) -> None: self.trainer.train_labeled_data() @@ -400,8 +403,5 @@ def train_labeled_data(self) -> None: def train_unlabeled_data(self) -> None: self.trainer.train_unlabeled_data() - def predict(self, image: np.ndarray) -> list: - return self.predictor.predict(image) - def predict_all(self) -> dict: return self.predictor.predict_all() \ No newline at end of file From 6194272b89031146e04e87343f23c2612840fb3d Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 17:08:03 +0900 Subject: [PATCH 20/25] Feat: prediction for image, encoding --- VisageSnap/main.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 9d84f3c..af55561 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -360,6 +360,19 @@ def __predict(self, image: np.ndarray) -> list: result.append(-1) return result + + def predict_image(self, image: np.ndarray) -> list: + assert isinstance(image, np.ndarray), "parameter must be numpy array." + + return self.__predict(image) + + def predict_encoding(self, encoding: np.ndarray) -> int: + assert isinstance(encoding, np.ndarray), "parameter must be numpy array." + + prediction = self.__predict([encoding]) + + return -1 if -1 in prediction else prediction[0] + def predict_all(self) -> dict: result = {} @@ -403,5 +416,11 @@ def train_labeled_data(self) -> None: def train_unlabeled_data(self) -> None: self.trainer.train_unlabeled_data() + def predict_encoding(self, encoding: np.ndarray) -> int: + return self.predictor.predict_encoding(encoding) + + def predict_image(self, image: np.ndarray) -> int: + return self.predictor.predict_image(image) + def predict_all(self) -> dict: return self.predictor.predict_all() \ No newline at end of file From dffe37614c2b3e0863d7520ec5dd2f88c8ce0efe Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 17:09:19 +0900 Subject: [PATCH 21/25] Chore: delete DirectoryManager --- VisageSnap/main.py | 69 ++-------------------------------------------- 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index af55561..35e7b1f 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -94,73 +94,7 @@ def get_faceObject(self, target: str, value: str) -> Face: if value in face.filenames: return face return None - -class DirectoryManager: - def __init__(self, directory: Directory): - self.__directory = directory - - @property - def dir(self) -> dict: - """ - This function returns the directory. - - Returns - ------- - dict (dict) : directory dictionary. - - Example - ------- - dict = { - "labeled": "labeled", - "unlabeled": "unlabeled", - "model": "model", - "predict": "predict" - } - - labeled : directory of the labeled data - - unlabeled : directory of the unlabeled data - - model : directory of the model - - predict : directory of the predict data - - Default - ------- - labeled : "labeled" - unlabeled : "unlabeled" - model : "model" - predict : "predict" - """ - return self.__directory - @dir.setter - def dir(self, dicto: dict) -> None: - def _set_dir(key: str, value: str): - # Check if directory exists - if os.path.isdir(value) == False: - raise("The directory does not exist.") - - # Check if value is absoulte path or relative path. if relative, convert to absolute path. - value = absp(value) if os.path.isabs(value) is False else value - - if key == "labeled": - self.__directory.labeled = value - elif key == "unlabeled": - self.__directory.unlabeled = value - elif key == "model": - self.__directory.model = value - elif key == "predict": - self.__directory.predict = value - - for key, value in dicto.items(): - if key == "labeled": - _set_dir(key, value) - elif key == "unlabeled": - _set_dir(key, value) - elif key == "model": - _set_dir(key, value) - elif key == "predict": - _set_dir(key, value) - else: - raise("The key is not valid.") - class ModelHandler: def __init__(self, globalState: GlobalState = None, directory: Directory = None): @@ -410,6 +344,9 @@ def __init__(self, globalState: GlobalState = None, directory: Directory = None) self.trainer = Trainer(self.__state, self.__directory, self.imageLoader, self.modelHandler) self.predictor = Predictor(self.__state, self.__directory) + def set_directory(self, directory: Directory) -> None: + self.__directory = directory + def train_labeled_data(self) -> None: self.trainer.train_labeled_data() From 19e51d8058b74a118d8763acd3297c9780d6f330 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 17:17:43 +0900 Subject: [PATCH 22/25] Chore: change classes.py to dataclasses.py --- VisageSnap/__init__.py | 2 +- VisageSnap/{classes.py => dataclasses.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename VisageSnap/{classes.py => dataclasses.py} (100%) diff --git a/VisageSnap/__init__.py b/VisageSnap/__init__.py index 90abc51..6fc5d9f 100644 --- a/VisageSnap/__init__.py +++ b/VisageSnap/__init__.py @@ -3,4 +3,4 @@ __license__ = "MIT" from .main import Core -from .classes import * \ No newline at end of file +from .dataclasses import * \ No newline at end of file diff --git a/VisageSnap/classes.py b/VisageSnap/dataclasses.py similarity index 100% rename from VisageSnap/classes.py rename to VisageSnap/dataclasses.py From 312d6f80899305cf5d8efa7207f14d0573387ddb Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 17:42:56 +0900 Subject: [PATCH 23/25] Chore: rename classes --- VisageSnap/__init__.py | 2 +- VisageSnap/{dataclasses.py => classes.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename VisageSnap/{dataclasses.py => classes.py} (100%) diff --git a/VisageSnap/__init__.py b/VisageSnap/__init__.py index 6fc5d9f..90abc51 100644 --- a/VisageSnap/__init__.py +++ b/VisageSnap/__init__.py @@ -3,4 +3,4 @@ __license__ = "MIT" from .main import Core -from .dataclasses import * \ No newline at end of file +from .classes import * \ No newline at end of file diff --git a/VisageSnap/dataclasses.py b/VisageSnap/classes.py similarity index 100% rename from VisageSnap/dataclasses.py rename to VisageSnap/classes.py From 79c9636265eacdc29101af868e79f93646ceccc3 Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 18:08:17 +0900 Subject: [PATCH 24/25] Refactor: separate classes --- VisageSnap/image/__init__.py | 1 + VisageSnap/image/imageloader.py | 60 +++++ VisageSnap/main.py | 354 ++------------------------ VisageSnap/model/__init__.py | 1 + VisageSnap/model/modelhandler.py | 32 +++ VisageSnap/processor/__init__.py | 3 + VisageSnap/processor/faceprocessor.py | 91 +++++++ VisageSnap/processor/predictor.py | 111 ++++++++ VisageSnap/processor/trainer.py | 45 ++++ 9 files changed, 369 insertions(+), 329 deletions(-) create mode 100644 VisageSnap/image/__init__.py create mode 100644 VisageSnap/image/imageloader.py create mode 100644 VisageSnap/model/__init__.py create mode 100644 VisageSnap/model/modelhandler.py create mode 100644 VisageSnap/processor/__init__.py create mode 100644 VisageSnap/processor/faceprocessor.py create mode 100644 VisageSnap/processor/predictor.py create mode 100644 VisageSnap/processor/trainer.py diff --git a/VisageSnap/image/__init__.py b/VisageSnap/image/__init__.py new file mode 100644 index 0000000..df5dffd --- /dev/null +++ b/VisageSnap/image/__init__.py @@ -0,0 +1 @@ +from .imageloader import ImageLoader \ No newline at end of file diff --git a/VisageSnap/image/imageloader.py b/VisageSnap/image/imageloader.py new file mode 100644 index 0000000..8efce91 --- /dev/null +++ b/VisageSnap/image/imageloader.py @@ -0,0 +1,60 @@ +from ..classes import Face, GlobalState, Directory, From, To, As +from ..utils import isimage, gen +import numpy as np +import face_recognition +import os + +class ImageLoader: + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + self.__state = globalState + self.__directory = directory + + def load_labeled(self) -> None: + """ + This function loads the labeled data from the labeled directory. + Only allows one face per image. + """ + for filename in gen(os.listdir(self.__directory.labeled)): + if isimage(filename): + label = (filename.split(".")[0]).split("-")[0] # 파일 형식은 이름-번호.jpg임. 이름만 추출 + image = face_recognition.load_image_file(os.path.join(self.__directory.labeled, filename)) + encodings = face_recognition.face_encodings(image) + encoding = encodings[0] + + # If there are more than one face, skip. + if len(encodings) > 1: + continue + + # Check if the face is already in the state. + FACE_FOUND = False + for i, face in enumerate(self.__state.faces): # Search for the face in the state + + if face.label == label: # If the face label is already in the state + for old_encoding in face.encodings: + + # Check if the encoding is already in the face. + if np.array_equal(old_encoding, encoding): + continue + + # If the encoding is not in the face, append it. + self.__state.faces[i].encodings.append(encoding) + self.__state.faces[i].filenames.append(filename) + FACE_FOUND = True + + if not FACE_FOUND: # If the face is not in the state, create new face. + self.__state.faces.append(Face(label, [encoding], [filename])) + + def load_unlabeled(self) -> None: + """ + This function loads the unlabeled data from the unlabeled directory. + """ + for filename in gen(os.listdir(self.__directory.unlabeled)): + if isimage(filename): + image = face_recognition.load_image_file(os.path.join(self.__directory.unlabeled, filename)) + encodings = face_recognition.face_encodings(image) + + if len(encodings) == 0: # If there is no face, skip. + continue + + for encoding in encodings: + self.__state.faces.append(Face("unknown", encoding, [filename])) diff --git a/VisageSnap/main.py b/VisageSnap/main.py index 35e7b1f..c7cbeeb 100644 --- a/VisageSnap/main.py +++ b/VisageSnap/main.py @@ -1,333 +1,9 @@ -import face_recognition -from dataclasses import dataclass -import os +from .model import ModelHandler +from .image import ImageLoader +from .processor import FaceProcessor, Trainer, Predictor +from .classes import GlobalState, Directory import numpy as np -from sklearn.semi_supervised import LabelPropagation -from sklearn.exceptions import NotFittedError -import pickle -from typing import Generator, Union -from .classes import * -from .utils import * -class FaceProcessor: - def __init__(self, globalState: GlobalState): - self.__state = globalState - - def gen_faces(self) -> Generator[Face, None, None]: - for face in self.__state.faces: - yield face - - def convert_labelType(self, value: Union[str, int], to: str) -> any: - """ - This function converts the label type. (numberLabel -> nameLabel, nameLabel -> numberLabel) - - Parameters - ---------- - value (str or int) : target value. - to (str) : - - "To.NAME" : convert to name label. - - "To.NUMBER" : convert to number label. - - Returns - ------- - str or int : converted value. (if To.NAME, return str, if To.NUMBER, return int) - """ - if to == "Name": - for name, number in self.__state.label.items(): - if number == value: - return name - elif to == "Number": - return self.__state.label.get(value, -1) - return None - - def set_label(self, person: Union[list, dict]) -> None: - """ - This function sets the label dictionary. - - Parameters - ---------- - person (list or dict) : label list or dictionary. - - Example - ------- - person = ["name1", "name2", "name3", ...] - - OR - - person = { - "name1": 0, - "name2": 1, - "name3": 2, - ... - } - - - name1, name2, name3, ... : name of the person - - 0, 1, 2, ... : number label (MUST NOT BE -1) - """ - - if type(person) == dict: - self.__state.label = person - elif type(person) == list: - for i in range(len(person)): - self.__state.label[person[i]] = i - - def get_faceObject(self, target: str, value: str) -> Face: - """ - This function returns the face object with the given label. - - Parameters - ---------- - target: - - "From.LABEL" : label of the face object. (name of the person) - - "From.FILENAME" : filename of the face object. - - value (str) : value of the target. - """ - assert isinstance(target, str), "target must be 'From.LABEL' or 'From.FILENAME'." - assert isinstance(value, str), "value must be a string." - - for face in self.gen_faces(): - if target == "Label": - if face.label == value: - return face - elif target == "Filename": - if value in face.filenames: - return face - return None - - -class ModelHandler: - def __init__(self, globalState: GlobalState = None, directory: Directory = None): - self.__state = globalState - self.__directory = directory - self.__state.model = self.load() - - def load(self) -> LabelPropagation: - try: - model_path = os.path.join(self.__directory.model, "face_model.pkl") - - with open(model_path, "rb") as f: - self.__state.model, self.__state.faces = pickle.load(f) - print("Model loaded.") - - except: - print("Model not found. Creating new model...") - self.__state.model = LabelPropagation() - self.__state.faces = [] # 초기화 - - return self.__state.model - - def save(self) -> None: - model_path = os.path.join(self.__directory.model, "face_model.pkl") - - # Create directory if not exists - os.mkdir(self.__directory.model) if not os.path.exists(self.__directory.model) else None - - with open(model_path, "wb") as f: - pickle.dump((self.__state.model, self.__state.faces), f) # Save as tuple - - -class ImageLoader: - def __init__(self, globalState: GlobalState = None, directory: Directory = None): - self.__state = globalState - self.__directory = directory - - def load_labeled(self) -> None: - """ - This function loads the labeled data from the labeled directory. - Only allows one face per image. - """ - for filename in gen(os.listdir(self.__directory.labeled)): - if isimage(filename): - label = (filename.split(".")[0]).split("-")[0] # 파일 형식은 이름-번호.jpg임. 이름만 추출 - image = face_recognition.load_image_file(os.path.join(self.__directory.labeled, filename)) - encodings = face_recognition.face_encodings(image) - encoding = encodings[0] - - # If there are more than one face, skip. - if len(encodings) > 1: - continue - - # Check if the face is already in the state. - FACE_FOUND = False - for i, face in enumerate(self.__state.faces): # Search for the face in the state - - if face.label == label: # If the face label is already in the state - for old_encoding in face.encodings: - - # Check if the encoding is already in the face. - if np.array_equal(old_encoding, encoding): - continue - - # If the encoding is not in the face, append it. - self.__state.faces[i].encodings.append(encoding) - self.__state.faces[i].filenames.append(filename) - FACE_FOUND = True - - if not FACE_FOUND: # If the face is not in the state, create new face. - self.__state.faces.append(Face(label, [encoding], [filename])) - - def load_unlabeled(self) -> None: - """ - This function loads the unlabeled data from the unlabeled directory. - """ - for filename in gen(os.listdir(self.__directory.unlabeled)): - if isimage(filename): - image = face_recognition.load_image_file(os.path.join(self.__directory.unlabeled, filename)) - encodings = face_recognition.face_encodings(image) - - if len(encodings) == 0: # If there is no face, skip. - continue - - for encoding in encodings: - self.__state.faces.append(Face("unknown", encoding, [filename])) - - -class Trainer(FaceProcessor): - def __init__(self, globalState: GlobalState = None, directory: Directory = None, imageLoader: ImageLoader = None, modelHandler: ModelHandler = None): - super().__init__(globalState) - - self.__state = globalState - self.__directory = directory - self.imageLoader = ImageLoader(self.__state, self.__directory) if imageLoader is None else imageLoader - self.modelHandler = ModelHandler(self.__state, self.__directory) if modelHandler is None else modelHandler - - def __train(self, labeled: bool) -> None: - assert isinstance(labeled, bool), "parameter must be boolean." - - if labeled: - self.imageLoader.load_labeled() - else: - self.imageLoader.load_unlabeled() - - t_names = [] - t_encodings =[] - - for face in self.gen_faces(): - for encoding in face.encodings: - numberLabel = self.convert_labelType(face.label, To.NUMBER) - if labeled and numberLabel == -1: # 라벨링 데이터 학습인데 unknown이면 학습하지 않음 - continue - t_names.append(numberLabel) - t_encodings.append(encoding) - - t_encodings = np.array(t_encodings) - t_names = np.array(t_names) - - self.__state.model.fit(t_encodings, t_names) - self.modelHandler.save() - - def train_labeled_data(self) -> None: - self.__train(As.LABELED) - - def train_unlabeled_data(self) -> None: - self.__train(As.UNLABELED) - -class Predictor(FaceProcessor): - def __init__(self, globalState: GlobalState = None, directory: Directory = None): - super().__init__(globalState) - - self.__state = globalState - self.__directory = directory - self.threshold = 0.48 - - @staticmethod - def __get_average(face: Face) -> np.array: - """ - This function returns the average of the encodings. - - Parameters - ---------- - face (Face) : target face. - """ - assert isinstance(face, Face), "parameter must be Face class." - return np.average(face.encodings, axis=0) - - @staticmethod - def __get_distance(encoding1: np.ndarray, encoding2: np.ndarray) -> float: - """ - This function returns the distance between two encodings. - - Parameters - ---------- - encoding1 (np.array) : encoding1. - encoding2 (np.array) : encoding2. - """ - assert isinstance(encoding1, np.ndarray), "parameter must be numpy array." - assert isinstance(encoding2, np.ndarray), "parameter must be numpy array." - - return np.linalg.norm(encoding1 - encoding2) - - def __isNotUnknown(self, encoding) -> bool: - """ - This function checks whether the encoding is unknown. - - Parameters - ---------- - encoding (np.array) : target encoding. - """ - assert isinstance(encoding, np.ndarray), "parameter must be numpy array." - - min_distance = 1 - for face in self.gen_faces(): - average = self.__get_average(face) # 저장된 얼굴 평균 구하고 - distance = self.__get_distance(encoding, average) # 타겟과의 거리를 구한다 - if distance < min_distance: - min_distance = distance - - if min_distance < self.threshold: - return True # 모르는 사람이 아니다 - return False # 모르는 사람이다 - - def __predict(self, image: np.ndarray) -> list: - assert isinstance(image, np.ndarray), "parameter must be numpy array." - - target_encodings = face_recognition.face_encodings(image) - if len(target_encodings) == 0: - return None - - result = [] - for target_encoding in target_encodings: - if self.__isNotUnknown(target_encoding): # 모르는 사람이 아니면 - result.append(self.__state.model.predict([target_encoding])[0]) - else: - result.append(-1) - - return result - - def predict_image(self, image: np.ndarray) -> list: - assert isinstance(image, np.ndarray), "parameter must be numpy array." - - return self.__predict(image) - - def predict_encoding(self, encoding: np.ndarray) -> int: - assert isinstance(encoding, np.ndarray), "parameter must be numpy array." - - prediction = self.__predict([encoding]) - - return -1 if -1 in prediction else prediction[0] - - - def predict_all(self) -> dict: - result = {} - for filename in os.listdir(self.__directory.predict): - if isimage(filename) == False: - return - - image = face_recognition.load_image_file(os.path.join(self.__directory.predict, filename)) - prediction = self.__predict(image) - - if prediction == None: - raise("There is no face in the image.") - - if len(prediction) == 1: - result[filename] = self.convert_labelType(prediction[0], To.NAME) - else: - result[filename] = [] - for p in prediction: - result[filename].append(self.convert_labelType(p, To.NAME)) - return result - class Core(FaceProcessor): """ VisageSnap Core Class @@ -347,17 +23,37 @@ def __init__(self, globalState: GlobalState = None, directory: Directory = None) def set_directory(self, directory: Directory) -> None: self.__directory = directory + def load_model(self) -> None: + """ + This function loads the model. + If there is no model, it will create a new model. + + Also you can set the model directory by using set_directory method. + """ + self.modelHandler.load() + + def is_model_loaded(self) -> bool: + """ + This function checks whether the model is loaded or not. + """ + return self.__state.model is not None + def train_labeled_data(self) -> None: + assert self.is_model_loaded(), "Model is not loaded." self.trainer.train_labeled_data() def train_unlabeled_data(self) -> None: + assert self.is_model_loaded(), "Model is not loaded." self.trainer.train_unlabeled_data() def predict_encoding(self, encoding: np.ndarray) -> int: + assert self.is_model_loaded(), "Model is not loaded." return self.predictor.predict_encoding(encoding) def predict_image(self, image: np.ndarray) -> int: + assert self.is_model_loaded(), "Model is not loaded." return self.predictor.predict_image(image) - + def predict_all(self) -> dict: + assert self.is_model_loaded(), "Model is not loaded." return self.predictor.predict_all() \ No newline at end of file diff --git a/VisageSnap/model/__init__.py b/VisageSnap/model/__init__.py new file mode 100644 index 0000000..7cd4f14 --- /dev/null +++ b/VisageSnap/model/__init__.py @@ -0,0 +1 @@ +from .modelhandler import ModelHandler \ No newline at end of file diff --git a/VisageSnap/model/modelhandler.py b/VisageSnap/model/modelhandler.py new file mode 100644 index 0000000..7fc00b7 --- /dev/null +++ b/VisageSnap/model/modelhandler.py @@ -0,0 +1,32 @@ +from ..classes import Face, GlobalState, Directory, From, To, As +from sklearn.semi_supervised import LabelPropagation +import pickle +import os + +class ModelHandler: + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + self.__state = globalState + self.__directory = directory + self.__state.model = None + + def load(self) -> None: + try: + model_path = os.path.join(self.__directory.model, "face_model.pkl") + + with open(model_path, "rb") as f: + self.__state.model, self.__state.faces = pickle.load(f) + print("Model loaded.") + + except: + print("Model not found. Creating new model...") + self.__state.model = LabelPropagation() + self.__state.faces = [] # 초기화 + + def save(self) -> None: + model_path = os.path.join(self.__directory.model, "face_model.pkl") + + # Create directory if not exists + os.mkdir(self.__directory.model) if not os.path.exists(self.__directory.model) else None + + with open(model_path, "wb") as f: + pickle.dump((self.__state.model, self.__state.faces), f) # Save as tuple \ No newline at end of file diff --git a/VisageSnap/processor/__init__.py b/VisageSnap/processor/__init__.py new file mode 100644 index 0000000..dcfd5fc --- /dev/null +++ b/VisageSnap/processor/__init__.py @@ -0,0 +1,3 @@ +from .faceprocessor import FaceProcessor +from .trainer import Trainer +from .predictor import Predictor \ No newline at end of file diff --git a/VisageSnap/processor/faceprocessor.py b/VisageSnap/processor/faceprocessor.py new file mode 100644 index 0000000..7241abe --- /dev/null +++ b/VisageSnap/processor/faceprocessor.py @@ -0,0 +1,91 @@ +from ..classes import Face, GlobalState, From, To, As +from typing import Generator, Union +import logging + +logger = logging.getLogger(__name__) + +class FaceProcessor: + def __init__(self, globalState: GlobalState): + self.__state = globalState + + def gen_faces(self) -> Generator[Face, None, None]: + for face in self.__state.faces: + yield face + + def convert_labelType(self, value: Union[str, int], to: str) -> any: + """ + This function converts the label type. (numberLabel -> nameLabel, nameLabel -> numberLabel) + + Parameters + ---------- + value (str or int) : target value. + to (str) : + - "To.NAME" : convert to name label. + - "To.NUMBER" : convert to number label. + + Returns + ------- + str or int : converted value. (if To.NAME, return str, if To.NUMBER, return int) + """ + if to == "Name": + for name, number in self.__state.label.items(): + if number == value: + return name + elif to == "Number": + return self.__state.label.get(value, -1) + return None + + def set_label(self, person: Union[list, dict]) -> None: + """ + This function sets the label dictionary. + + Parameters + ---------- + person (list or dict) : label list or dictionary. + + Example + ------- + person = ["name1", "name2", "name3", ...] + + OR + + person = { + "name1": 0, + "name2": 1, + "name3": 2, + ... + } + + - name1, name2, name3, ... : name of the person + - 0, 1, 2, ... : number label (MUST NOT BE -1) + """ + + if type(person) == dict: + self.__state.label = person + elif type(person) == list: + for i in range(len(person)): + self.__state.label[person[i]] = i + + def get_faceObject(self, target: str, value: str) -> Face: + """ + This function returns the face object with the given label. + + Parameters + ---------- + target: + - "From.LABEL" : label of the face object. (name of the person) + - "From.FILENAME" : filename of the face object. + + value (str) : value of the target. + """ + assert isinstance(target, str), "target must be 'From.LABEL' or 'From.FILENAME'." + assert isinstance(value, str), "value must be a string." + + for face in self.gen_faces(): + if target == "Label": + if face.label == value: + return face + elif target == "Filename": + if value in face.filenames: + return face + return None \ No newline at end of file diff --git a/VisageSnap/processor/predictor.py b/VisageSnap/processor/predictor.py new file mode 100644 index 0000000..af1fb9d --- /dev/null +++ b/VisageSnap/processor/predictor.py @@ -0,0 +1,111 @@ +from ..classes import Face, GlobalState, Directory, From, To, As +from ..utils import isimage +from .faceprocessor import FaceProcessor +import numpy as np +import face_recognition +import os + +class Predictor(FaceProcessor): + def __init__(self, globalState: GlobalState = None, directory: Directory = None): + super().__init__(globalState) + + self.__state = globalState + self.__directory = directory + self.threshold = 0.48 + + @staticmethod + def __get_average(face: Face) -> np.array: + """ + This function returns the average of the encodings. + + Parameters + ---------- + face (Face) : target face. + """ + assert isinstance(face, Face), "parameter must be Face class." + return np.average(face.encodings, axis=0) + + @staticmethod + def __get_distance(encoding1: np.ndarray, encoding2: np.ndarray) -> float: + """ + This function returns the distance between two encodings. + + Parameters + ---------- + encoding1 (np.array) : encoding1. + encoding2 (np.array) : encoding2. + """ + assert isinstance(encoding1, np.ndarray), "parameter must be numpy array." + assert isinstance(encoding2, np.ndarray), "parameter must be numpy array." + + return np.linalg.norm(encoding1 - encoding2) + + def __isNotUnknown(self, encoding) -> bool: + """ + This function checks whether the encoding is unknown. + + Parameters + ---------- + encoding (np.array) : target encoding. + """ + assert isinstance(encoding, np.ndarray), "parameter must be numpy array." + + min_distance = 1 + for face in self.gen_faces(): + average = self.__get_average(face) # 저장된 얼굴 평균 구하고 + distance = self.__get_distance(encoding, average) # 타겟과의 거리를 구한다 + if distance < min_distance: + min_distance = distance + + if min_distance < self.threshold: + return True # 모르는 사람이 아니다 + return False # 모르는 사람이다 + + def __predict(self, image: np.ndarray) -> list: + assert isinstance(image, np.ndarray), "parameter must be numpy array." + + target_encodings = face_recognition.face_encodings(image) + if len(target_encodings) == 0: + return None + + result = [] + for target_encoding in target_encodings: + if self.__isNotUnknown(target_encoding): # 모르는 사람이 아니면 + result.append(self.__state.model.predict([target_encoding])[0]) + else: + result.append(-1) + + return result + + def predict_image(self, image: np.ndarray) -> list: + assert isinstance(image, np.ndarray), "parameter must be numpy array." + + return self.__predict(image) + + def predict_encoding(self, encoding: np.ndarray) -> int: + assert isinstance(encoding, np.ndarray), "parameter must be numpy array." + + prediction = self.__predict([encoding]) + + return -1 if -1 in prediction else prediction[0] + + + def predict_all(self) -> dict: + result = {} + for filename in os.listdir(self.__directory.predict): + if isimage(filename) == False: + return + + image = face_recognition.load_image_file(os.path.join(self.__directory.predict, filename)) + prediction = self.__predict(image) + + if prediction == None: + raise("There is no face in the image.") + + if len(prediction) == 1: + result[filename] = self.convert_labelType(prediction[0], To.NAME) + else: + result[filename] = [] + for p in prediction: + result[filename].append(self.convert_labelType(p, To.NAME)) + return result \ No newline at end of file diff --git a/VisageSnap/processor/trainer.py b/VisageSnap/processor/trainer.py new file mode 100644 index 0000000..c73489c --- /dev/null +++ b/VisageSnap/processor/trainer.py @@ -0,0 +1,45 @@ +from ..classes import Face, GlobalState, Directory, From, To, As +from .faceprocessor import FaceProcessor +from ..image import ImageLoader +from ..model import ModelHandler +import numpy as np + +class Trainer(FaceProcessor): + def __init__(self, globalState: GlobalState = None, directory: Directory = None, imageLoader: ImageLoader = None, modelHandler: ModelHandler = None): + super().__init__(globalState) + + self.__state = globalState + self.__directory = directory + self.imageLoader = ImageLoader(self.__state, self.__directory) if imageLoader is None else imageLoader + self.modelHandler = ModelHandler(self.__state, self.__directory) if modelHandler is None else modelHandler + + def __train(self, labeled: bool) -> None: + assert isinstance(labeled, bool), "parameter must be boolean." + + if labeled: + self.imageLoader.load_labeled() + else: + self.imageLoader.load_unlabeled() + + t_names = [] + t_encodings =[] + + for face in self.gen_faces(): + for encoding in face.encodings: + numberLabel = self.convert_labelType(face.label, To.NUMBER) + if labeled and numberLabel == -1: # 라벨링 데이터 학습인데 unknown이면 학습하지 않음 + continue + t_names.append(numberLabel) + t_encodings.append(encoding) + + t_encodings = np.array(t_encodings) + t_names = np.array(t_names) + + self.__state.model.fit(t_encodings, t_names) + self.modelHandler.save() + + def train_labeled_data(self) -> None: + self.__train(As.LABELED) + + def train_unlabeled_data(self) -> None: + self.__train(As.UNLABELED) \ No newline at end of file From 1ec38f815191045b48f3dda6daa51de01d2ee90c Mon Sep 17 00:00:00 2001 From: asheswook Date: Mon, 29 May 2023 18:08:33 +0900 Subject: [PATCH 25/25] Test: add load_model code --- tests/test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test.py b/tests/test.py index 0cb1776..62995f7 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,6 +1,7 @@ import VisageSnap vs = VisageSnap.Core() +vs.load_model() vs.set_label(["NY", "JY"]) vs.train_labeled_data() result = vs.predict_all()