commit 4c6c422ba5169588465f0253e3214cd68631ee9b Author: yumoqing Date: Tue Jun 24 11:49:37 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..9922869 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# FindPerson +a module to save and find face in image +* find and save all the face in image and save them in milvus vector database +* find all similarity faces in vector database with face in a given image + +## dependent +see the requirements.txt + +## installation +``` +pip install git+https://github.com/yumoqing/findperson +``` + + diff --git a/app/imagefind.py b/app/imagefind.py new file mode 100644 index 0000000..f36e972 --- /dev/null +++ b/app/imagefind.py @@ -0,0 +1,15 @@ +from ahserver.webapp import webapp +from ahserver.serverenv import ServerEnv +from appPublic.worker import awaitify +from findperson.init import load_findperson + +def get_module_dbname(name): + return 'imagefind' + +def init(): + g = ServerEnv() + g.get_module_dbname = get_module_dbname + load_findperson + +if __name__ == '__main__': + webapp(init) diff --git a/conf/config.json b/conf/config.json new file mode 100644 index 0000000..7796acb --- /dev/null +++ b/conf/config.json @@ -0,0 +1,53 @@ +{ + "logger":{ + "name":"rag", + "levelname":"info", + "logfile":"$[workdir]$/logs/pcapi.log" + }, + "vectordb_path":"$[workdir]$/vdb", + "authentication":{ + "user":"kyycloud", + "password":"Kyy@123456", + "iplist":[ + "47.93.12.75", + "127.0.0.1", + "117.50.205.57", + "10.60.179.61" + ] + }, + "filesroot":"$[workdir]$/files", + "website":{ + "paths":[ + ["$[workdir]$/wwwroot",""] + ], + "client_max_size":10000, + "host":"0.0.0.0", + "port":20001, + "coding":"utf-8", + "indexes":[ + "index.ui", + "index.dspy", + "index.md" + ], + "startswiths":[ + { + "leading":"/idfile", + "registerfunction":"idFileDownload" + } + ], + "processors":[ + [".ui","bui"], + [".dspy","dspy"], + [".md","md"] + ], + "session_max_time":3000, + "session_issue_time":2500 + }, + "langMapping":{ + "zh-Hans-CN":"zh-cn", + "zh-CN":"zh-cn", + "en-us":"en", + "en-US":"en" + } +} + diff --git a/findperson/__init__.py b/findperson/__init__.py new file mode 100644 index 0000000..1f56709 --- /dev/null +++ b/findperson/__init__.py @@ -0,0 +1,2 @@ +from .version import __version__ + diff --git a/findperson/image_imbedding.py b/findperson/image_imbedding.py new file mode 100644 index 0000000..f0edec2 --- /dev/null +++ b/findperson/image_imbedding.py @@ -0,0 +1,50 @@ +import face_recognition +import numpy as np +from sklearn.decomposition import PCA +from appPublic.uniqueID import getID + +def expand_to_768_zero_padding(embedding): + return np.pad(embedding, (0, 768 - 128), mode="constant") + +class ImageImbedding: + def get_image_faces_vector(self, image_path): + image = face_recognition.load_image_file(image_path) + face_encodings = face_recognition.face_encodings(image, face_locations) # 计算 128 维特征 + return face_encodings + + def image2faces(self, image_path, imgid=None): + image = face_recognition.load_image_file(image_path) + + # 检测人脸并提取嵌入向量 + face_locations = face_recognition.face_locations(image) # 找到所有人脸 + face_encodings = face_recognition.face_encodings(image, face_locations) # 计算 128 维特征 + if imgid is None: + imgid = getID() + faces = [] + ret = { + "id": imgid, + "image": image_path, + "faces": faces + } + for i, v in enumerate(face_encodings): + # print('vector v has', v.shape, 'shape', v) + # v_768 = expand_to_768_zero_padding(v) + # print(v.shape, v_768.shape) + top, right, bottom, left = face_locations[i] + face = { + "id": getID(), + "imgid": imgid, + "imagepath":image_path, + "vector": v, + "left": left, + "top": top, + "right": right, + "bottom": bottom + } + faces.append(face) + return ret + + +if __name__ == '__main__': + ii = ImageImbedding() + v = ii.image2vector('test.png') diff --git a/findperson/imageface.py b/findperson/imageface.py new file mode 100644 index 0000000..6700736 --- /dev/null +++ b/findperson/imageface.py @@ -0,0 +1,65 @@ +import json +import face_recognition +from PIL import Image +from appPublic.jsonConfig import getConfig +from appPublic.dictObject import DictObject + +from .image_imbedding import ImageImbedding +from .vectordb import MilvusVectorDB + +class ImageFaces: + """ + implements image to faces and to vector and save the face's vector to vector database + and search similarities faces in vector database with faces in argument image_path + """ + def __init__(self): + self.vdb = MilvusVectorDB() + self.ii = ImageImbedding() + + def save_faces(self, userid, image_path, imgid=None): + """ + find all the faces in image identified by image_path, + and save the face's info to vector database + """ + v = self.ii.image2faces(image_path, imgid=imgid) + for face in v['faces']: + face['userid'] = userid + self.vdb.add('faces', face) + return v + + def find_face_in_image(self, image_path, limit=5): + """ + similarities search for all the faces in image identified by image_path + return faces info in image attached similarities face's info + """ + self.vdb.create_vector_index('faces') + v = self.ii.image2faces(image_path) + for face in v['faces']: + ret = self.vdb.search_by_vector('faces', face['vector'], limit=limit) + ret = [DictObject(**d) for d in ret.pop()] + return ret + return [] + +if __name__ == '__main__': + import sys + from appPublic.jsonConfig import getConfig + config = getConfig('./', NS={'workdir':'.'}) + if len(sys.argv) < 3: + print(f'{sys.argv[0]} COMMAND imgfile ...') + sys.exit(1) + act = sys.argv[1] + i_f = ImageFaces('oooo1') + if act == 'add': + for f in sys.argv[2:]: + v = i_f.save_faces(f) + else: + fif = sys.argv[2] + v = i_f.find_face_in_image(fif) + print(v['id'], v['image']) + for face in v['faces']: + t = face['similarities'] + for d in t: + print(f'{d=}') + print(f"{face['id']=},{face['imgid']=}") + + diff --git a/findperson/init.py b/findperson/init.py new file mode 100644 index 0000000..ce7415b --- /dev/null +++ b/findperson/init.py @@ -0,0 +1,13 @@ +from ahserver.serverenv import ServerEnv +from appPublic.worker import awaitify +from .imageface import ImageFaces +from .utils_clip import CLIPEmbedder + +def load_findperson(): + g = ServerEnv() + ifs = ImageFaces() + ce = CLIPEmbedder() + g.find_face_in_image = awaitify(ifs.find_face_in_image) + g.save_faces = awaitify(ifs.save_faces) + g.embed_image = ce.embed_image + g.embed_text = ce.embed_text diff --git a/findperson/milvus_utils.py b/findperson/milvus_utils.py new file mode 100644 index 0000000..decbde3 --- /dev/null +++ b/findperson/milvus_utils.py @@ -0,0 +1,127 @@ +from appPublic.jsonConfig import getConfig +from appPublic.dictObject import DictObject +from pymilvus import MilvusClient, DataType, FieldSchema, CollectionSchema + +class MilvusCollection: + def __init__(self, client, name, dimension): + self.client = client + self.name = name, + self.dimension = dimension + sellf.set_fields() + self.set_indexes() + self.vectorfield=None + self.output_fields = [] + + def set_fields(self): + self.fields = [] + + def set_indexes(): + self.indexes = [] + + def create_vector_index(self): + index_params = self.client.prepare_index_params() + for idx in self.indexes: + index_params.add_index(**idx) + self.client.create_index(self.name, index_params) + + def create_table_if_not_exists(self): + if not self.client.has_collection(collection_name=self.name): + fields = [] + for f in self.fields: + f = DictObject(**f) + if f.dtype == DataType.FLOAT_VECTOR: + self.vectorfield = f.name + f.dim=self.dimension + else: + self.output_fields.append(f.name) + fields.append(FieldSchema(**f)) + schema = CollectionSchema(fields=fields,auto_id=False, enable_dynamic_field=True) + self.client.create_collection( + collection_name=self.name, + dimension=self.dimension, + schema=schema + ) + + def add(self, ns,flush=False): + self.create_table_if_not_exists(self.name) + ret = self.client.insert(collection_name=self.name, data=ns) + if flush: + self.create_vector_index(self.name) + return ret + + def search_by_vector(self, vector, limit=5): + self.create_table_if_not_exists(self.name) + return self.client.search( + collection_name=self.name, + anns_field=self.vectorfield, + data=[vector], + output_fields=self.output_fields, + limit=limit + ) + + def delete(self, id): + self.create_table_if_not_exists(self.name) + return self.client.delete( + collection_name=self.name, + ids=[id] + ) + +class MilvusVectorDB: + def __init__(self, dimension=128): + config = getConfig() + dbname = config.vectordb_path + self.dbname = dbname + self.dimension = dimension + self.client = MilvusClient(dbname) + +class Faces(MilvusCollection): + def set_fields(self): + self.fields = [ + dict(name='id',dtype=DataType.VARCHAR, + auto_id=False, + is_primary=True, max_length=34), + dict(name='vector', dtype=DataType.FLOAT_VECTOR, + dim=self.dimension, description='vector'), + dict(name='imgid', dtype=DataType.VARCHAR, max_length=34), + dict(name='imagepath', dtype=DataType.VARCHAR, max_length=500), + dict(name='top', dtype=DataType.INT32), + dict(name='left', dtype=DataType.INT32), + dict(name='right', dtype=DataType.INT32), + dict(name='bottom', dtype=DataType.INT32), + dict(name='userid', dtype=DataType.VARCHAR, max_length=34) + ] + def set_indexs(self): + self.indexes = { + "index_type": "IVF_FLAT", # Choose index type + "field_name":"vector", + "index_name":"vector_index", + # Distance metric: L2 (Euclidean) or IP (Inner Product) + "metric_type": "L2", + "params": {"nlist": 128} # Number of clusters for IVF + } + +class ImageVector(MilvusCollection): + def set_fields(self): + self.fields = [ + dict(name='id',dtype=DataType.VARCHAR, + auto_id=False, + is_primary=True, max_length=34), + dict(name='vector', dtype=DataType.FLOAT_VECTOR, + dim=self.dimension, description='vector'), + dict(name='imgid', dtype=DataType.VARCHAR, max_length=34), + dict(name='imagepath', dtype=DataType.VARCHAR, max_length=500), + dict(name='top', dtype=DataType.INT32), + dict(name='left', dtype=DataType.INT32), + dict(name='right', dtype=DataType.INT32), + dict(name='bottom', dtype=DataType.INT32), + dict(name='userid', dtype=DataType.VARCHAR, max_length=34) + ] + def set_indexs(self): + self.indexes = { + "index_type": "IVF_FLAT", # Choose index type + "field_name":"vector", + "index_name":"vector_index", + # Distance metric: L2 (Euclidean) or IP (Inner Product) + "metric_type": "L2", + "params": {"nlist": 128} # Number of clusters for IVF + } diff --git a/findperson/utils_clip.py b/findperson/utils_clip.py new file mode 100644 index 0000000..49278c9 --- /dev/null +++ b/findperson/utils_clip.py @@ -0,0 +1,27 @@ +from appPublic.jsonConfig import getConfig +from transformers import CLIPProcessor, CLIPModel +from PIL import Image +import torch + +class CLIPEmbedder: + def __init__(self): + # model_id="laion/CLIP-ViT-H-14-laion2B-s32B-b79K" + self.config = getConfig() + model_path = self.config.clip_model_path + self.device = "cuda" if torch.cuda.is_available() else "cpu" + self.model = CLIPModel.from_pretrained(model_path).to(self.device) + self.processor = CLIPProcessor.from_pretrained(model_path) + + def embed_image(self, image_path): + image = Image.open(image_path).convert("RGB") + inputs = self.processor(images=image, return_tensors="pt").to(self.device) + with torch.no_grad(): + embedding = self.model.get_image_features(**inputs) + return embedding[0].cpu().numpy() + + def embed_text(self, text): + inputs = self.processor(text=text, return_tensors="pt").to(self.device) + with torch.no_grad(): + embedding = self.model.get_text_features(**inputs) + return embedding[0].cpu().numpy() + diff --git a/findperson/vectordb.py b/findperson/vectordb.py new file mode 100644 index 0000000..ab3dfe3 --- /dev/null +++ b/findperson/vectordb.py @@ -0,0 +1,70 @@ +from appPublic.jsonConfig import getConfig +from pymilvus import MilvusClient, DataType, FieldSchema, CollectionSchema + +class MilvusVectorDB: + def __init__(self, dimension=128): + config = getConfig() + dbname = config.vectordb_path + self.dbname = dbname + self.dimension = dimension + self.client = MilvusClient(dbname) + + def create_vector_index(self, tblname): + self.client.flush(tblname) + index_params = self.client.prepare_index_params() + index_params.add_index(**{ + "index_type": "IVF_FLAT", # Choose index type + "field_name":"vector", + "index_name":"vector_index", + "metric_type": "L2", # Distance metric: L2 (Euclidean) or IP (Inner Product) + "params": {"nlist": 128} # Number of clusters for IVF + }) + self.client.create_index(tblname, index_params) + print("index created") + + def create_table_if_not_exists(self, tblname): + if not self.client.has_collection(collection_name=tblname): + fields = [ + FieldSchema(name='id',dtype=DataType.VARCHAR, + auto_id=False, + is_primary=True, max_length=34), + FieldSchema(name='vector', dtype=DataType.FLOAT_VECTOR, + dim=self.dimension, description='vector'), + FieldSchema(name='imgid', dtype=DataType.VARCHAR, max_length=34), + FieldSchema(name='imagepath', dtype=DataType.VARCHAR, max_length=500), + FieldSchema(name='top', dtype=DataType.INT32), + FieldSchema(name='left', dtype=DataType.INT32), + FieldSchema(name='right', dtype=DataType.INT32), + FieldSchema(name='bottom', dtype=DataType.INT32), + FieldSchema(name='userid', dtype=DataType.VARCHAR, max_length=34) + ] + schema = CollectionSchema(fields=fields,auto_id=False, enable_dynamic_field=True) + self.client.create_collection( + collection_name=tblname, + dimension=self.dimension, + schema=schema + ) + + def add(self, tblname, ns,flush=False): + self.create_table_if_not_exists(tblname) + ret = self.client.insert(collection_name=tblname, data=ns) + if flush: + self.create_vector_index(tblname) + return ret + + def search_by_vector(self, tblname, vector, limit=5): + self.create_table_if_not_exists(tblname) + return self.client.search( + collection_name=tblname, + anns_field="vector", + data=[vector], + output_fields=["imgid", "imagepath", "top","right","bottom", "left", "userid" ], + limit=limit + ) + + def delete(self, tblname, id): + self.create_table_if_not_exists(tblname) + return self.client.delete( + collection_name=tblname, + ids=[id] + ) diff --git a/findperson/version.py b/findperson/version.py new file mode 100644 index 0000000..b8023d8 --- /dev/null +++ b/findperson/version.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eebdd32 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# +pillow +face_recognition +pymilvus +