diff --git a/ketrface/cluster.py b/ketrface/cluster.py new file mode 100644 index 0000000..bb566b5 --- /dev/null +++ b/ketrface/cluster.py @@ -0,0 +1,158 @@ +import sys +import json +import os +import piexif +import sqlite3 +from sqlite3 import Error +from PIL import Image +import numpy as np +from deepface import DeepFace +from deepface.detectors import FaceDetector +import functools + +from ketrface.util import * +from ketrface.dbscan import * +from ketrface.db import * + +html_base = '../' +db_path = '../db/photos.db' + +# TODO +# Switch to using DBSCAN +# +# Thoughts for determining number of clusters to try and target... +# +# Augment DBSCAN to rule out identity matching for the same face +# appearing more than once in a photo +# +# NOTE: This means twins or reflections won't both identify in the +# same photo -- those faces would then identify as a second face pairing +# which could merge with a cluster, but can not be used to match + + +def gen_html(identities): + for identity in identities: + + print('
') + print(f'
Identity {identity["id"]} has {len(identity["faces"])}
') + print('
') + for face in identity['faces']: + faceId = face['id'] + photoId = face['photoId'] + distance = "{:0.4f}".format(face['distance']) + confidence = "{:0.3f}".format(face['confidence']) + label = face['cluster'] + if type(label) != str: + label = f'Cluster ({face["cluster"]["id"]})' + + print('
') + path = f'{html_base}/faces/{"{:02d}".format(faceId % 10)}' + print(f'') + print(f'
{label}: {distance}
') + print(f'
{faceId} {photoId} {confidence}
') + print('
') + print('
') + print('
') + + +def load_faces(db_path = db_path): + conn = create_connection(db_path) + faces = [] + with conn: + cur = conn.cursor() + res = cur.execute(''' + SELECT faces.id,facedescriptors.descriptors,faces.faceConfidence,faces.photoId + FROM faces + JOIN facedescriptors ON (faces.descriptorId=facedescriptors.id) + WHERE faces.identityId IS null AND faces.faceConfidence>0.99 + ''') + for row in res.fetchall(): + id, descriptors, confidence, photoId = row + face = { + 'id': id, + 'type': 'face', + 'confidence': confidence, + 'distance': 0, + 'photoId': photoId, + 'descriptors': np.frombuffer(descriptors), + 'cluster': Undefined + } + face['faces'] = [ face ] + faces.append(face) + return faces + +def cluster_sort(A, B): + diff = A['cluster'] - B['cluster'] + if diff > 0: + return 1 + elif diff < 0: + return -1 + diff = A['confidence'] - B['confidence'] + if diff > 0: + return 1 + elif diff < 0: + return -1 + return 0 + +print('Loading faces from database') +faces = load_faces() +print(f'{len(faces)} faces loaded') +print('Scanning for clusters') +identities = DBSCAN(faces) # process_faces(faces) +print(f'{len(identities)} clusters grouped') + + + +# Compute average center for all clusters +sum = 0 +for identity in identities: + sum += len(identity['faces']) + print(f'{identity["id"]} has {len(identity["faces"])} faces') + average = [] + + for face in identity['faces']: + if len(average) == 0: + average = face['descriptors'] + else: + average = np.add(average, face['descriptors']) + + average = np.divide(average, len(identity['faces'])) + identity['descriptors'] = average + +removed = -1 +epoch = 1 +# Filter each cluster removing any face that is > cluster_max_distance +# from the average center point of the cluster +while removed != 0: + print(f'Epoch {epoch}...') + epoch += 1 + removed = 0 + for identity in identities: + for face in identity['faces']: + average = identity['descriptors'] + distance = findCosineDistance(average, face['descriptors']) + if distance > 0.14: + average = np.dot(average, len(identity['faces'])) + average = np.subtract(average, face['descriptors']) + + face['cluster'] = Undefined + face['distance'] = 0 + identity['faces'].remove(face) + + identity['descriptors'] = np.divide(average, len(identity['faces'])) + removed += 1 + else: + face['distance'] = distance + if removed > 0: + print(f'Excluded {removed} faces this epoch') + +identities.sort(reverse = True, key = lambda x: len(x['faces'])) +for identity in identities: + identity['faces'].sort(reverse = False, key = lambda x: x['distance']) + +print(f'{len(identities)} identities seeded.') + +print('Writing to "identities.html"') +redirect_on('identities.html') +gen_html(identities) +redirect_off() diff --git a/ketrface/detect.py b/ketrface/detect.py new file mode 100644 index 0000000..f05d78d --- /dev/null +++ b/ketrface/detect.py @@ -0,0 +1,248 @@ +import sys +import zlib +import json +import os +import piexif + +from PIL import Image, ImageOps +from deepface import DeepFace +from deepface.detectors import FaceDetector +from retinaface import RetinaFace +import numpy as np +import cv2 +from ketrface.util import * +from ketrface.db import * + +model_name = 'VGG-Face' # 'ArcFace' +detector_backend = 'mtcnn' # 'retinaface' +model = DeepFace.build_model(model_name) + +# Derived from https://github.com/serengil/deepface/blob/master/deepface/detectors/MtcnnWrapper.py +# Add parameters to MTCNN +from mtcnn import MTCNN +face_detector = MTCNN(min_face_size = 30) +input_shape = DeepFace.functions.find_input_shape(model) + +# Adapted from DeepFace +# https://github.com/serengil/deepface/blob/master/deepface/commons/functions.py +# +# Modified to use bicubic resampling and clip expansion, as well as to +# take a PIL Image instead of numpy array +def alignment_procedure(img, left_eye, right_eye): + """ + Given left and right eye coordinates in image, rotate around point + between eyes such that eyes are horizontal + :param img: Image (not np.array) + :param left_eye: Eye appearing on the left (right eye of person) + :param right_eye: Eye appearing on the right (left eye of person) + :return: adjusted image + """ + dY = right_eye[1] - left_eye[1] + dX = right_eye[0] - left_eye[0] + radians = np.arctan2(dY, dX) + rotation = 180 + 180 * radians / np.pi + + if True: + img = img.rotate( + angle = rotation, + resample = Image.BICUBIC, + expand = True) + + return img + +def variance_of_laplacian(image): + # compute the Laplacian of the image and then return the focus + # measure, which is simply the variance of the Laplacian + return cv2.Laplacian(image, cv2.CV_64F).var() + +def extract_faces(img, threshold=0.95, allow_upscaling = True, focus_threshold = 100): + if detector_backend == 'retinaface': + faces = RetinaFace.detect_faces( + img_path = img, + threshold = threshold, + model = model, + allow_upscaling = allow_upscaling) + elif detector_backend == 'mtcnn': + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # mtcnn expects RGB + + redirect_on() + res = face_detector.detect_faces(img_rgb) + redirect_off() + + faces = {} + if type(res) == list: + for i, face in enumerate(res): + if threshold > face['confidence']: + continue + x = face['box'][0] + y = face['box'][1] + w = face['box'][2] + h = face['box'][3] + # If face is less than 2.5% of the image width and height, + # skip it (too small) -- filters out likely blurry faces in + # large group photos where the actual face may exceed + # min_face_size passed to MTCNN + if 0.025 > w / img.shape[0] and 0.025 > h / img.shape[1]: + print(f'Dropping due to small face size: {w / img.shape[0]} x {h / img.shape[1]}') + continue + faces[f'face_{i+1}'] = { # standardize properties + 'facial_area': [ x, y, x + w, y + h ], + 'landmarks': { + 'left_eye': list(face['keypoints']['left_eye']), + 'right_eye': list(face['keypoints']['right_eye']), + }, + 'score': face['confidence'], + } + + # Re-implementation of 'extract_faces' with the addition of keeping a + # copy of the face image for caching on disk + for k, key in enumerate(faces): + print(f'Processing face {k+1}/{len(faces)}') + identity = faces[key] + facial_area = identity["facial_area"] + landmarks = identity["landmarks"] + left_eye = landmarks["left_eye"] + right_eye = landmarks["right_eye"] + +# markup = True + markup = False + if markup == True: # Draw the face rectangle and eyes + cv2.rectangle(img, + (int(facial_area[0]), int(facial_area[1])), + (int(facial_area[2]), int(facial_area[3])), + (0, 0, 255), 2) + cv2.circle(img, (int(left_eye[0]), int(left_eye[1])), 5, (255, 0, 0), 2) + cv2.circle(img, (int(right_eye[0]), int(right_eye[1])), 5, (0, 255, 0), 2) + + # Find center of face, then crop to square + # of equal width and height + width = facial_area[2] - facial_area[0] + height = facial_area[3] - facial_area[1] + x = facial_area[0] + width * 0.5 + y = facial_area[1] + height * 0.5 + + # Make thumbnail a square crop + if width > height: + height = width + else: + width = height + + #width *= 1.25 + #height *= 1.25 + + left = max(round(x - width * 0.5), 0) + right = min(round(left + width), img.shape[1]) # Y is 1 + top = max(round(y - height * 0.5), 0) + bottom = min(round(top + height), img.shape[0]) # X is 0 + + left_eye[0] -= top + left_eye[1] -= left + right_eye[0] -= top + right_eye[1] -= left + + facial_img = img[top: bottom, left: right] + + image = Image.fromarray(facial_img) + + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + focus = variance_of_laplacian(gray) + if focus < focus_threshold: + print(f'Dropping {ke+1} due to focus {focus}.') + faces.pop(key) + identity['focus'] = focus + + # Eye order is reversed as the routine does them backwards + image = alignment_procedure(image, right_eye, left_eye) + image = image.resize(size = input_shape, resample = Image.LANCZOS) + resized = np.asarray(image) + + redirect_on() + identity['vector'] = DeepFace.represent( + img_path = resized, + model_name = model_name, + model = model, # pre-built + detector_backend = detector_backend, + enforce_detection = False) + redirect_off() + + identity["face"] = { + 'top': facial_area[1] / img.shape[0], + 'left': facial_area[0] / img.shape[1], + 'bottom': facial_area[3] / img.shape[0], + 'right': facial_area[2] / img.shape[1] + } + + identity['image'] = Image.fromarray(resized) + + return faces + + +base = '/pictures/' +conn = create_connection('../db/photos.db') +with conn: + cur = conn.cursor() + res = cur.execute(''' + SELECT photos.id,photos.faces,albums.path,photos.filename FROM photos + LEFT JOIN albums ON (albums.id=photos.albumId) + WHERE photos.faces=-1 + ''') + rows = res.fetchall() + count = len(rows) + for i, row in enumerate(rows): + photoId, photoFaces, albumPath, photoFilename = row + img_path = f'{base}{albumPath}{photoFilename}' + print(f'Processing {i+1}/{count}: {img_path}') + img = Image.open(img_path) + img = ImageOps.exif_transpose(img) # auto-rotate if needed + img = img.convert() + img = np.asarray(img) + faces = extract_faces(img) + if faces is None: + print(f'Image no faces: {img_path}') + update_face_count(conn, photoId, 0) + continue + for j, key in enumerate(faces): + face = faces[key] + image = face['image'] + print(f'Writing face {j+1}/{len(faces)}') + + #face['analysis'] = DeepFace.analyze(img_path = img, actions = ['age', 'gender', 'race', 'emotion'], enforce_detection = False) + #face['analysis'] = DeepFace.analyze(img, actions = ['emotion']) + + # TODO: Add additional meta-data allowing back referencing to original + # photo + face['version'] = 1 # version 1 doesn't add much... + + data = {k: face[k] for k in set(list(face.keys())) - set(['image', 'facial_area', 'landmarks'])} + json_str = json.dumps(data, ensure_ascii=False, cls=NpEncoder) + faceDescriptorId = create_face_descriptor(conn, face) + + faceId = create_face(conn, { + 'photoId': photoId, + 'scanVersion': face['version'], + 'faceConfidence': face['score'], + 'top': face['face']['top'], + 'left': face['face']['left'], + 'bottom': face['face']['bottom'], + 'right': face['face']['right'], + 'descriptorId': faceDescriptorId, + }) + + path = f'faces/{"{:02d}".format(faceId % 10)}' + try: + os.mkdir(path) + except FileExistsError: + pass + + with open(f'{path}/{faceId}.json', 'w', encoding = 'utf-8') as f: + f.write(json_str) + + compressed_str = zlib_uuencode(json_str.encode()) + + # Encode this data into the JPG as Exif + exif_ifd = {piexif.ExifIFD.UserComment: compressed_str} + exif_dict = {"0th": {}, "Exif": exif_ifd, "1st": {}, + "thumbnail": None, "GPS": {}} + image.save(f'{path}/{faceId}.jpg', exif = piexif.dump(exif_dict)) + + update_face_count(conn, photoId, len(faces)) diff --git a/server/headers.py b/ketrface/headers.py similarity index 63% rename from server/headers.py rename to ketrface/headers.py index 04ab8c8..2f9eac9 100644 --- a/server/headers.py +++ b/ketrface/headers.py @@ -6,12 +6,7 @@ from PIL import Image import uu from io import BytesIO -def zlib_uudecode(databytes): - ''' uudecode databytes and decompress the result with zlib ''' - inbuff = BytesIO(databytes) - outbuff = BytesIO() - uu.decode(inbuff, outbuff) - return zlib.decompress(outbuff.getvalue()) +from ketrface.util import * faceId = int(sys.argv[1]) path = f'faces/{"{:02d}".format(faceId % 10)}' diff --git a/ketrface/identities.html b/ketrface/identities.html new file mode 100644 index 0000000..f2fd3be --- /dev/null +++ b/ketrface/identities.html @@ -0,0 +1,4170 @@ +
+
Identity 1 has 130
+
+
+ +
Cluster (1): 0.0492
+
5312 269 0.992
+
+
+ +
Cluster (1): 0.0515
+
5143 207 0.997
+
+
+ +
Cluster (1): 0.0520
+
4393 1 0.999
+
+
+ +
Cluster (1): 0.0535
+
5019 168 0.998
+
+
+ +
Cluster (1): 0.0539
+
5375 294 0.996
+
+
+ +
Cluster (1): 0.0542
+
5150 208 1.000
+
+
+ +
Cluster (1): 0.0550
+
4588 63 0.994
+
+
+ +
Cluster (1): 0.0558
+
5425 303 0.992
+
+
+ +
Cluster (1): 0.0561
+
5810 391 1.000
+
+
+ +
Cluster (1): 0.0576
+
5878 404 0.992
+
+
+ +
Cluster (1): 0.0588
+
5011 165 1.000
+
+
+ +
Cluster (1): 0.0592
+
4875 130 0.999
+
+
+ +
Cluster (1): 0.0610
+
5110 198 1.000
+
+
+ +
Cluster (1): 0.0619
+
5058 182 1.000
+
+
+ +
Cluster (1): 0.0627
+
5844 398 0.994
+
+
+ +
Cluster (1): 0.0644
+
5065 184 0.991
+
+
+ +
Cluster (1): 0.0645
+
4998 156 0.994
+
+
+ +
Cluster (1): 0.0653
+
5092 191 0.999
+
+
+ +
Cluster (1): 0.0670
+
5294 252 0.995
+
+
+ +
Cluster (1): 0.0673
+
5287 247 0.999
+
+
+ +
Cluster (1): 0.0677
+
5727 365 0.994
+
+
+ +
Cluster (1): 0.0679
+
5095 193 0.999
+
+
+ +
Cluster (1): 0.0688
+
5849 399 0.999
+
+
+ +
Cluster (1): 0.0691
+
5094 192 1.000
+
+
+ +
Cluster (1): 0.0696
+
5102 194 0.999
+
+
+ +
Cluster (1): 0.0698
+
5082 188 0.999
+
+
+ +
Cluster (1): 0.0706
+
4897 132 0.998
+
+
+ +
Cluster (1): 0.0707
+
4581 60 1.000
+
+
+ +
Cluster (1): 0.0716
+
5880 405 1.000
+
+
+ +
Cluster (1): 0.0722
+
5086 189 0.994
+
+
+ +
Cluster (1): 0.0725
+
5873 403 0.998
+
+
+ +
Cluster (1): 0.0729
+
5835 396 0.996
+
+
+ +
Cluster (1): 0.0730
+
5183 218 1.000
+
+
+ +
Cluster (1): 0.0733
+
5047 176 0.999
+
+
+ +
Cluster (1): 0.0742
+
5116 199 0.992
+
+
+ +
Cluster (1): 0.0744
+
5269 240 0.998
+
+
+ +
Cluster (1): 0.0746
+
5136 204 0.997
+
+
+ +
Cluster (1): 0.0753
+
4826 119 1.000
+
+
+ +
Cluster (1): 0.0757
+
5559 331 1.000
+
+
+ +
Cluster (1): 0.0758
+
5419 302 0.999
+
+
+ +
Cluster (1): 0.0759
+
5674 355 0.997
+
+
+ +
Cluster (1): 0.0765
+
5430 306 0.994
+
+
+ +
Cluster (1): 0.0768
+
5329 272 0.999
+
+
+ +
Cluster (1): 0.0773
+
5865 402 0.999
+
+
+ +
Cluster (1): 0.0783
+
5055 181 0.996
+
+
+ +
Cluster (1): 0.0787
+
5052 179 0.999
+
+
+ +
Cluster (1): 0.0792
+
5898 414 0.999
+
+
+ +
Cluster (1): 0.0795
+
5008 163 0.996
+
+
+ +
Cluster (1): 0.0799
+
4405 5 0.995
+
+
+ +
Cluster (1): 0.0803
+
5125 201 0.994
+
+
+ +
Cluster (1): 0.0809
+
5438 308 1.000
+
+
+ +
Cluster (1): 0.0811
+
4798 109 0.992
+
+
+ +
Cluster (1): 0.0811
+
4776 97 0.999
+
+
+ +
Cluster (1): 0.0821
+
5069 186 0.999
+
+
+ +
Cluster (1): 0.0823
+
5426 304 1.000
+
+
+ +
Cluster (1): 0.0828
+
5332 274 1.000
+
+
+ +
Cluster (1): 0.0837
+
4906 135 0.994
+
+
+ +
Cluster (1): 0.0843
+
5577 332 0.996
+
+
+ +
Cluster (1): 0.0849
+
4971 151 0.996
+
+
+ +
Cluster (1): 0.0858
+
5138 205 0.999
+
+
+ +
Cluster (1): 0.0858
+
5238 231 0.994
+
+
+ +
Cluster (1): 0.0866
+
4812 115 1.000
+
+
+ +
Cluster (1): 0.0877
+
5118 200 0.998
+
+
+ +
Cluster (1): 0.0878
+
5160 209 0.995
+
+
+ +
Cluster (1): 0.0883
+
5289 251 0.999
+
+
+ +
Cluster (1): 0.0885
+
5089 190 0.999
+
+
+ +
Cluster (1): 0.0890
+
4921 136 0.997
+
+
+ +
Cluster (1): 0.0894
+
4895 131 0.998
+
+
+ +
Cluster (1): 0.0899
+
5204 219 0.997
+
+
+ +
Cluster (1): 0.0912
+
5043 174 0.999
+
+
+ +
Cluster (1): 0.0929
+
5863 401 0.997
+
+
+ +
Cluster (1): 0.0930
+
5045 175 0.999
+
+
+ +
Cluster (1): 0.0937
+
5223 226 0.998
+
+
+ +
Cluster (1): 0.0944
+
5499 319 0.997
+
+
+ +
Cluster (1): 0.0950
+
4859 122 0.995
+
+
+ +
Cluster (1): 0.0959
+
5592 337 0.997
+
+
+ +
Cluster (1): 0.0970
+
5493 318 0.995
+
+
+ +
Cluster (1): 0.0977
+
5757 379 0.999
+
+
+ +
Cluster (1): 0.0979
+
4586 62 1.000
+
+
+ +
Cluster (1): 0.0980
+
5182 217 0.998
+
+
+ +
Cluster (1): 0.0981
+
5831 395 0.995
+
+
+ +
Cluster (1): 0.0993
+
5323 270 0.998
+
+
+ +
Cluster (1): 0.1022
+
5452 312 0.999
+
+
+ +
Cluster (1): 0.1024
+
4771 96 0.991
+
+
+ +
Cluster (1): 0.1028
+
5131 202 0.993
+
+
+ +
Cluster (1): 0.1037
+
4808 113 0.997
+
+
+ +
Cluster (1): 0.1039
+
5406 298 0.997
+
+
+ +
Cluster (1): 0.1053
+
4925 137 0.999
+
+
+ +
Cluster (1): 0.1058
+
5409 299 1.000
+
+
+ +
Cluster (1): 0.1062
+
5309 267 0.999
+
+
+ +
Cluster (1): 0.1066
+
5170 213 0.998
+
+
+ +
Cluster (1): 0.1066
+
5676 356 0.998
+
+
+ +
Cluster (1): 0.1066
+
4987 154 0.999
+
+
+ +
Cluster (1): 0.1088
+
5752 377 0.999
+
+
+ +
Cluster (1): 0.1095
+
5003 158 0.999
+
+
+ +
Cluster (1): 0.1095
+
5894 411 1.000
+
+
+ +
Cluster (1): 0.1099
+
4873 129 0.999
+
+
+ +
Cluster (1): 0.1102
+
4749 89 0.996
+
+
+ +
Cluster (1): 0.1108
+
4656 76 0.995
+
+
+ +
Cluster (1): 0.1111
+
5542 327 0.992
+
+
+ +
Cluster (1): 0.1115
+
5830 394 1.000
+
+
+ +
Cluster (1): 0.1116
+
5133 203 0.999
+
+
+ +
Cluster (1): 0.1125
+
5245 234 1.000
+
+
+ +
Cluster (1): 0.1131
+
4646 73 1.000
+
+
+ +
Cluster (1): 0.1137
+
4978 152 1.000
+
+
+ +
Cluster (1): 0.1151
+
5208 220 0.995
+
+
+ +
Cluster (1): 0.1153
+
4538 50 0.998
+
+
+ +
Cluster (1): 0.1165
+
5347 282 0.999
+
+
+ +
Cluster (1): 0.1176
+
5220 225 0.999
+
+
+ +
Cluster (1): 0.1177
+
4932 141 1.000
+
+
+ +
Cluster (1): 0.1197
+
5893 410 0.996
+
+
+ +
Cluster (1): 0.1202
+
5972 429 0.995
+
+
+ +
Cluster (1): 0.1208
+
5164 210 0.995
+
+
+ +
Cluster (1): 0.1223
+
5054 180 1.000
+
+
+ +
Cluster (1): 0.1229
+
5804 389 0.999
+
+
+ +
Cluster (1): 0.1234
+
5336 278 0.995
+
+
+ +
Cluster (1): 0.1258
+
4697 82 0.996
+
+
+ +
Cluster (1): 0.1291
+
5340 280 0.994
+
+
+ +
Cluster (1): 0.1296
+
5597 341 0.997
+
+
+ +
Cluster (1): 0.1299
+
5715 363 0.998
+
+
+ +
Cluster (1): 0.1310
+
5669 351 0.993
+
+
+ +
Cluster (1): 0.1315
+
4796 108 0.992
+
+
+ +
Cluster (1): 0.1337
+
5707 360 0.993
+
+
+ +
Cluster (1): 0.1344
+
5534 326 0.996
+
+
+ +
Cluster (1): 0.1348
+
4699 83 0.999
+
+
+ +
Cluster (1): 0.1348
+
4934 142 1.000
+
+
+ +
Cluster (1): 0.1352
+
4833 120 0.992
+
+
+ +
Cluster (1): 0.1374
+
4999 157 0.999
+
+
+ +
Cluster (1): 0.1392
+
5549 329 0.998
+
+
+ +
Cluster (1): 0.1398
+
5551 330 1.000
+
+
+
+
+
Identity 8 has 101
+
+
+ +
Cluster (8): 0.0356
+
5896 411 0.998
+
+
+ +
Cluster (8): 0.0384
+
5081 188 1.000
+
+
+ +
Cluster (8): 0.0405
+
5866 402 0.999
+
+
+ +
Cluster (8): 0.0451
+
5111 198 0.998
+
+
+ +
Cluster (8): 0.0453
+
5105 196 1.000
+
+
+ +
Cluster (8): 0.0457
+
5049 177 0.995
+
+
+ +
Cluster (8): 0.0467
+
5222 226 0.999
+
+
+ +
Cluster (8): 0.0473
+
5214 223 1.000
+
+
+ +
Cluster (8): 0.0474
+
5051 179 0.999
+
+
+ +
Cluster (8): 0.0476
+
5811 391 1.000
+
+
+ +
Cluster (8): 0.0500
+
5655 350 0.999
+
+
+ +
Cluster (8): 0.0501
+
5498 319 0.998
+
+
+ +
Cluster (8): 0.0511
+
5899 414 0.997
+
+
+ +
Cluster (8): 0.0515
+
5427 304 1.000
+
+
+ +
Cluster (8): 0.0517
+
5848 399 0.999
+
+
+ +
Cluster (8): 0.0537
+
5833 396 0.998
+
+
+ +
Cluster (8): 0.0547
+
5046 175 0.998
+
+
+ +
Cluster (8): 0.0561
+
5071 186 0.999
+
+
+ +
Cluster (8): 0.0568
+
5219 225 1.000
+
+
+ +
Cluster (8): 0.0578
+
5805 389 0.999
+
+
+ +
Cluster (8): 0.0608
+
5096 193 0.999
+
+
+ +
Cluster (8): 0.0610
+
5033 173 1.000
+
+
+ +
Cluster (8): 0.0615
+
5753 378 1.000
+
+
+ +
Cluster (8): 0.0622
+
5205 219 0.996
+
+
+ +
Cluster (8): 0.0639
+
5079 187 0.997
+
+
+ +
Cluster (8): 0.0640
+
5140 207 1.000
+
+
+ +
Cluster (8): 0.0646
+
5246 234 0.998
+
+
+ +
Cluster (8): 0.0649
+
5326 270 0.994
+
+
+ +
Cluster (8): 0.0667
+
5276 243 1.000
+
+
+ +
Cluster (8): 0.0679
+
4996 156 0.998
+
+
+ +
Cluster (8): 0.0682
+
5053 180 1.000
+
+
+ +
Cluster (8): 0.0686
+
5709 361 1.000
+
+
+ +
Cluster (8): 0.0699
+
5176 216 0.998
+
+
+ +
Cluster (8): 0.0700
+
4898 132 0.997
+
+
+ +
Cluster (8): 0.0708
+
5599 342 0.999
+
+
+ +
Cluster (8): 0.0708
+
4865 126 1.000
+
+
+ +
Cluster (8): 0.0712
+
5064 184 0.998
+
+
+ +
Cluster (8): 0.0719
+
5093 192 1.000
+
+
+ +
Cluster (8): 0.0722
+
5579 333 1.000
+
+
+ +
Cluster (8): 0.0723
+
4832 120 0.999
+
+
+ +
Cluster (8): 0.0738
+
5009 164 0.999
+
+
+ +
Cluster (8): 0.0752
+
5743 373 1.000
+
+
+ +
Cluster (8): 0.0761
+
4874 130 0.999
+
+
+ +
Cluster (8): 0.0766
+
4790 106 1.000
+
+
+ +
Cluster (8): 0.0775
+
5500 320 1.000
+
+
+ +
Cluster (8): 0.0792
+
5519 326 1.000
+
+
+ +
Cluster (8): 0.0794
+
5716 363 0.997
+
+
+ +
Cluster (8): 0.0798
+
5154 208 0.998
+
+
+ +
Cluster (8): 0.0800
+
5059 182 1.000
+
+
+ +
Cluster (8): 0.0803
+
5884 406 0.998
+
+
+ +
Cluster (8): 0.0805
+
4830 119 0.999
+
+
+ +
Cluster (8): 0.0806
+
4951 150 1.000
+
+
+ +
Cluster (8): 0.0808
+
4821 117 1.000
+
+
+ +
Cluster (8): 0.0812
+
5158 209 0.999
+
+
+ +
Cluster (8): 0.0814
+
5091 191 1.000
+
+
+ +
Cluster (8): 0.0815
+
5099 194 1.000
+
+
+ +
Cluster (8): 0.0817
+
4792 108 1.000
+
+
+ +
Cluster (8): 0.0822
+
4845 122 1.000
+
+
+ +
Cluster (8): 0.0824
+
4862 125 0.999
+
+
+ +
Cluster (8): 0.0826
+
4718 87 1.000
+
+
+ +
Cluster (8): 0.0849
+
4440 9 1.000
+
+
+ +
Cluster (8): 0.0851
+
5841 398 0.999
+
+
+ +
Cluster (8): 0.0857
+
4705 84 1.000
+
+
+ +
Cluster (8): 0.0859
+
4412 6 0.997
+
+
+ +
Cluster (8): 0.0865
+
5688 358 1.000
+
+
+ +
Cluster (8): 0.0870
+
5606 344 1.000
+
+
+ +
Cluster (8): 0.0870
+
4797 109 0.999
+
+
+ +
Cluster (8): 0.0875
+
5491 318 0.997
+
+
+ +
Cluster (8): 0.0903
+
4912 136 1.000
+
+
+ +
Cluster (8): 0.0917
+
4437 8 1.000
+
+
+ +
Cluster (8): 0.0919
+
5913 415 0.999
+
+
+ +
Cluster (8): 0.0925
+
5171 213 0.998
+
+
+ +
Cluster (8): 0.0929
+
5361 288 1.000
+
+
+ +
Cluster (8): 0.0958
+
4883 131 1.000
+
+
+ +
Cluster (8): 0.0960
+
5048 176 0.999
+
+
+ +
Cluster (8): 0.0961
+
4592 65 0.999
+
+
+ +
Cluster (8): 0.0966
+
5012 166 1.000
+
+
+ +
Cluster (8): 0.0977
+
4537 50 1.000
+
+
+ +
Cluster (8): 0.0999
+
5990 434 0.993
+
+
+ +
Cluster (8): 0.1013
+
5283 245 1.000
+
+
+ +
Cluster (8): 0.1035
+
5704 360 1.000
+
+
+ +
Cluster (8): 0.1042
+
5879 405 1.000
+
+
+ +
Cluster (8): 0.1044
+
5697 359 1.000
+
+
+ +
Cluster (8): 0.1049
+
5135 204 1.000
+
+
+ +
Cluster (8): 0.1057
+
5066 185 1.000
+
+
+ +
Cluster (8): 0.1059
+
5210 221 0.999
+
+
+ +
Cluster (8): 0.1072
+
4630 70 1.000
+
+
+ +
Cluster (8): 0.1086
+
5792 386 1.000
+
+
+ +
Cluster (8): 0.1126
+
5120 201 1.000
+
+
+ +
Cluster (8): 0.1156
+
4525 43 0.999
+
+
+ +
Cluster (8): 0.1158
+
4657 76 0.994
+
+
+ +
Cluster (8): 0.1169
+
4528 44 0.998
+
+
+ +
Cluster (8): 0.1183
+
5456 314 0.999
+
+
+ +
Cluster (8): 0.1195
+
5837 397 1.000
+
+
+ +
Cluster (8): 0.1201
+
5766 380 0.999
+
+
+ +
Cluster (8): 0.1202
+
5015 167 1.000
+
+
+ +
Cluster (8): 0.1206
+
5862 401 0.998
+
+
+ +
Cluster (8): 0.1227
+
4523 42 1.000
+
+
+ +
Cluster (8): 0.1260
+
4533 48 0.998
+
+
+ +
Cluster (8): 0.1305
+
4421 7 0.999
+
+
+ +
Cluster (8): 0.1328
+
4637 71 0.999
+
+
+
+
+
Identity 12 has 42
+
+
+ +
Cluster (12): 0.0610
+
4549 53 0.994
+
+
+ +
Cluster (12): 0.0683
+
4454 20 0.999
+
+
+ +
Cluster (12): 0.0795
+
4654 76 1.000
+
+
+ +
Cluster (12): 0.0812
+
4547 53 0.998
+
+
+ +
Cluster (12): 0.0862
+
4518 40 0.995
+
+
+ +
Cluster (12): 0.0867
+
4753 90 0.992
+
+
+ +
Cluster (12): 0.0876
+
4841 122 1.000
+
+
+ +
Cluster (12): 0.0880
+
4512 39 1.000
+
+
+ +
Cluster (12): 0.0899
+
4464 21 0.996
+
+
+ +
Cluster (12): 0.0902
+
5383 297 1.000
+
+
+ +
Cluster (12): 0.0928
+
4544 53 1.000
+
+
+ +
Cluster (12): 0.0937
+
4647 74 1.000
+
+
+ +
Cluster (12): 0.0945
+
4752 90 0.995
+
+
+ +
Cluster (12): 0.0946
+
4510 38 0.998
+
+
+ +
Cluster (12): 0.0984
+
4837 122 1.000
+
+
+ +
Cluster (12): 0.0996
+
5387 297 0.999
+
+
+ +
Cluster (12): 0.1004
+
5298 259 1.000
+
+
+ +
Cluster (12): 0.1005
+
5852 400 0.991
+
+
+ +
Cluster (12): 0.1015
+
4860 122 0.993
+
+
+ +
Cluster (12): 0.1054
+
4691 80 0.999
+
+
+ +
Cluster (12): 0.1061
+
5254 236 0.998
+
+
+ +
Cluster (12): 0.1097
+
4672 79 0.999
+
+
+ +
Cluster (12): 0.1099
+
4764 94 0.996
+
+
+ +
Cluster (12): 0.1100
+
4851 122 1.000
+
+
+ +
Cluster (12): 0.1123
+
4673 79 0.999
+
+
+ +
Cluster (12): 0.1134
+
4854 122 1.000
+
+
+ +
Cluster (12): 0.1144
+
4548 53 0.994
+
+
+ +
Cluster (12): 0.1156
+
4855 122 0.999
+
+
+ +
Cluster (12): 0.1174
+
5812 392 1.000
+
+
+ +
Cluster (12): 0.1174
+
4553 54 0.995
+
+
+ +
Cluster (12): 0.1175
+
4846 122 1.000
+
+
+ +
Cluster (12): 0.1201
+
4839 122 1.000
+
+
+ +
Cluster (12): 0.1201
+
4677 79 0.998
+
+
+ +
Cluster (12): 0.1212
+
4670 79 1.000
+
+
+ +
Cluster (12): 0.1216
+
5395 297 0.995
+
+
+ +
Cluster (12): 0.1216
+
4539 52 0.997
+
+
+ +
Cluster (12): 0.1280
+
5390 297 0.998
+
+
+ +
Cluster (12): 0.1291
+
4551 54 0.998
+
+
+ +
Cluster (12): 0.1320
+
5590 337 0.998
+
+
+ +
Cluster (12): 0.1357
+
5963 425 0.997
+
+
+ +
Cluster (12): 0.1361
+
5817 392 0.994
+
+
+ +
Cluster (12): 0.1376
+
5227 228 0.995
+
+
+
+
+
Identity 5 has 19
+
+
+ +
Cluster (5): 0.0644
+
5643 350 0.999
+
+
+ +
Cluster (5): 0.0668
+
4441 9 0.995
+
+
+ +
Cluster (5): 0.0684
+
4401 4 1.000
+
+
+ +
Cluster (5): 0.0725
+
4438 8 0.995
+
+
+ +
Cluster (5): 0.0754
+
4443 10 0.997
+
+
+ +
Cluster (5): 0.0758
+
4709 84 0.998
+
+
+ +
Cluster (5): 0.0802
+
5906 415 1.000
+
+
+ +
Cluster (5): 0.0819
+
5560 331 1.000
+
+
+ +
Cluster (5): 0.0841
+
5508 324 1.000
+
+
+ +
Cluster (5): 0.0842
+
5689 358 1.000
+
+
+ +
Cluster (5): 0.0852
+
4696 82 1.000
+
+
+ +
Cluster (5): 0.0920
+
4933 142 1.000
+
+
+ +
Cluster (5): 0.0929
+
5973 430 1.000
+
+
+ +
Cluster (5): 0.1020
+
5870 403 1.000
+
+
+ +
Cluster (5): 0.1088
+
5699 359 0.998
+
+
+ +
Cluster (5): 0.1100
+
4825 119 1.000
+
+
+ +
Cluster (5): 0.1114
+
4717 87 1.000
+
+
+ +
Cluster (5): 0.1333
+
5518 326 1.000
+
+
+ +
Cluster (5): 0.1390
+
5991 434 0.990
+
+
+
+
+
Identity 6 has 17
+
+
+ +
Cluster (6): 0.0613
+
5641 350 1.000
+
+
+ +
Cluster (6): 0.0624
+
5117 200 0.999
+
+
+ +
Cluster (6): 0.0645
+
4702 84 1.000
+
+
+ +
Cluster (6): 0.0672
+
5070 186 0.999
+
+
+ +
Cluster (6): 0.0759
+
4402 4 0.999
+
+
+ +
Cluster (6): 0.0832
+
5211 222 1.000
+
+
+ +
Cluster (6): 0.0897
+
5909 415 0.999
+
+
+ +
Cluster (6): 0.0927
+
5713 363 1.000
+
+
+ +
Cluster (6): 0.0934
+
5690 358 0.999
+
+
+ +
Cluster (6): 0.1004
+
5515 326 1.000
+
+
+ +
Cluster (6): 0.1079
+
5989 434 0.994
+
+
+ +
Cluster (6): 0.1094
+
5721 365 1.000
+
+
+ +
Cluster (6): 0.1106
+
5975 430 0.996
+
+
+ +
Cluster (6): 0.1168
+
5213 223 1.000
+
+
+ +
Cluster (6): 0.1179
+
5781 384 0.998
+
+
+ +
Cluster (6): 0.1289
+
5318 270 1.000
+
+
+ +
Cluster (6): 0.1386
+
4634 70 0.997
+
+
+
+
+
Identity 9 has 17
+
+
+ +
Cluster (9): 0.0634
+
5056 181 0.990
+
+
+ +
Cluster (9): 0.0690
+
5562 331 0.999
+
+
+ +
Cluster (9): 0.0698
+
4816 116 1.000
+
+
+ +
Cluster (9): 0.0710
+
4948 149 0.999
+
+
+ +
Cluster (9): 0.0800
+
5212 222 0.996
+
+
+ +
Cluster (9): 0.0829
+
4997 156 0.997
+
+
+ +
Cluster (9): 0.0836
+
5077 187 0.999
+
+
+ +
Cluster (9): 0.0876
+
4849 122 1.000
+
+
+ +
Cluster (9): 0.0921
+
4769 96 0.999
+
+
+ +
Cluster (9): 0.0922
+
4892 131 0.999
+
+
+ +
Cluster (9): 0.0943
+
5999 436 1.000
+
+
+ +
Cluster (9): 0.0948
+
4706 84 0.999
+
+
+ +
Cluster (9): 0.1028
+
5914 415 0.998
+
+
+ +
Cluster (9): 0.1089
+
4427 7 0.998
+
+
+ +
Cluster (9): 0.1156
+
4734 88 0.999
+
+
+ +
Cluster (9): 0.1197
+
5327 270 0.992
+
+
+ +
Cluster (9): 0.1264
+
5800 388 0.993
+
+
+
+
+
Identity 11 has 17
+
+
+ +
Cluster (11): 0.0766
+
4576 59 1.000
+
+
+ +
Cluster (11): 0.0790
+
5396 297 0.995
+
+
+ +
Cluster (11): 0.0821
+
4455 20 0.997
+
+
+ +
Cluster (11): 0.0829
+
4602 67 0.992
+
+
+ +
Cluster (11): 0.0887
+
5388 297 0.999
+
+
+ +
Cluster (11): 0.0977
+
4559 56 0.999
+
+
+ +
Cluster (11): 0.0988
+
4462 21 0.998
+
+
+ +
Cluster (11): 0.1028
+
4856 122 0.999
+
+
+ +
Cluster (11): 0.1068
+
4578 59 0.997
+
+
+ +
Cluster (11): 0.1078
+
5286 246 0.994
+
+
+ +
Cluster (11): 0.1129
+
4448 16 0.993
+
+
+ +
Cluster (11): 0.1147
+
5818 392 0.992
+
+
+ +
Cluster (11): 0.1178
+
4751 90 0.999
+
+
+ +
Cluster (11): 0.1241
+
4552 54 0.995
+
+
+ +
Cluster (11): 0.1289
+
5389 297 0.998
+
+
+ +
Cluster (11): 0.1336
+
4561 56 0.993
+
+
+ +
Cluster (11): 0.1395
+
5358 287 0.992
+
+
+
+
+
Identity 40 has 17
+
+
+ +
Cluster (40): 0.0917
+
4707 84 0.998
+
+
+ +
Cluster (40): 0.0933
+
4979 152 1.000
+
+
+ +
Cluster (40): 0.0979
+
5525 326 0.999
+
+
+ +
Cluster (40): 0.1022
+
5647 350 0.999
+
+
+ +
Cluster (40): 0.1032
+
5610 344 0.999
+
+
+ +
Cluster (40): 0.1108
+
5782 384 0.996
+
+
+ +
Cluster (40): 0.1160
+
5113 199 0.999
+
+
+ +
Cluster (40): 0.1175
+
5443 310 1.000
+
+
+ +
Cluster (40): 0.1193
+
5693 358 0.994
+
+
+ +
Cluster (40): 0.1215
+
5455 314 1.000
+
+
+ +
Cluster (40): 0.1225
+
5587 336 1.000
+
+
+ +
Cluster (40): 0.1227
+
5615 345 0.998
+
+
+ +
Cluster (40): 0.1236
+
6008 436 0.999
+
+
+ +
Cluster (40): 0.1245
+
5039 173 0.997
+
+
+ +
Cluster (40): 0.1287
+
5887 409 1.000
+
+
+ +
Cluster (40): 0.1371
+
5486 318 0.999
+
+
+ +
Cluster (40): 0.1383
+
5479 318 1.000
+
+
+
+
+
Identity 0 has 16
+
+
+ +
Cluster (0): 0.0634
+
5134 203 0.998
+
+
+ +
Cluster (0): 0.0677
+
5194 219 1.000
+
+
+ +
Cluster (0): 0.0708
+
4813 115 0.999
+
+
+ +
Cluster (0): 0.0726
+
5829 394 1.000
+
+
+ +
Cluster (0): 0.0777
+
4822 119 1.000
+
+
+ +
Cluster (0): 0.0875
+
4876 130 0.999
+
+
+ +
Cluster (0): 0.0911
+
4804 112 1.000
+
+
+ +
Cluster (0): 0.0929
+
5703 360 1.000
+
+
+ +
Cluster (0): 0.0983
+
4913 136 1.000
+
+
+ +
Cluster (0): 0.1004
+
4937 144 0.998
+
+
+ +
Cluster (0): 0.1032
+
5163 210 1.000
+
+
+ +
Cluster (0): 0.1119
+
5165 211 1.000
+
+
+ +
Cluster (0): 0.1132
+
5912 415 0.999
+
+
+ +
Cluster (0): 0.1133
+
4724 88 1.000
+
+
+ +
Cluster (0): 0.1166
+
4392 1 0.999
+
+
+ +
Cluster (0): 0.1349
+
6001 436 1.000
+
+
+
+
+
Identity 7 has 14
+
+
+ +
Cluster (7): 0.0846
+
5484 318 0.999
+
+
+ +
Cluster (7): 0.0916
+
4919 136 0.998
+
+
+ +
Cluster (7): 0.0949
+
5085 189 0.998
+
+
+ +
Cluster (7): 0.0957
+
5098 194 1.000
+
+
+ +
Cluster (7): 0.0992
+
5376 294 0.994
+
+
+ +
Cluster (7): 0.1008
+
5132 203 1.000
+
+
+ +
Cluster (7): 0.1009
+
4981 153 0.999
+
+
+ +
Cluster (7): 0.1019
+
4410 6 0.997
+
+
+ +
Cluster (7): 0.1023
+
4424 7 0.998
+
+
+ +
Cluster (7): 0.1023
+
5563 331 0.999
+
+
+ +
Cluster (7): 0.1027
+
5196 219 0.999
+
+
+ +
Cluster (7): 0.1158
+
5337 280 1.000
+
+
+ +
Cluster (7): 0.1174
+
5258 238 1.000
+
+
+ +
Cluster (7): 0.1283
+
5521 326 1.000
+
+
+
+
+
Identity 50 has 14
+
+
+ +
Cluster (50): 0.0612
+
5139 207 1.000
+
+
+ +
Cluster (50): 0.0729
+
5122 201 0.999
+
+
+ +
Cluster (50): 0.0767
+
4843 122 1.000
+
+
+ +
Cluster (50): 0.0835
+
5355 286 0.997
+
+
+ +
Cluster (50): 0.0859
+
4891 131 0.999
+
+
+ +
Cluster (50): 0.0915
+
5189 219 1.000
+
+
+ +
Cluster (50): 0.0958
+
5291 252 1.000
+
+
+ +
Cluster (50): 0.1021
+
5062 184 1.000
+
+
+ +
Cluster (50): 0.1052
+
5903 415 1.000
+
+
+ +
Cluster (50): 0.1136
+
5129 202 1.000
+
+
+ +
Cluster (50): 0.1190
+
5316 270 1.000
+
+
+ +
Cluster (50): 0.1252
+
4907 136 1.000
+
+
+ +
Cluster (50): 0.1276
+
5188 219 1.000
+
+
+ +
Cluster (50): 0.1303
+
5041 173 0.994
+
+
+
+
+
Identity 10 has 12
+
+
+ +
Cluster (10): 0.0646
+
4918 136 0.999
+
+
+ +
Cluster (10): 0.0689
+
5532 326 0.998
+
+
+ +
Cluster (10): 0.0713
+
5686 358 1.000
+
+
+ +
Cluster (10): 0.0813
+
5638 350 1.000
+
+
+ +
Cluster (10): 0.0892
+
4428 7 0.998
+
+
+ +
Cluster (10): 0.0924
+
5457 314 0.998
+
+
+ +
Cluster (10): 0.0939
+
5700 359 0.996
+
+
+ +
Cluster (10): 0.0974
+
5016 167 0.999
+
+
+ +
Cluster (10): 0.1079
+
5917 415 0.998
+
+
+ +
Cluster (10): 0.1154
+
5324 270 0.997
+
+
+ +
Cluster (10): 0.1264
+
5772 380 0.993
+
+
+ +
Cluster (10): 0.1312
+
5996 436 1.000
+
+
+
+
+
Identity 25 has 11
+
+
+ +
Cluster (25): 0.0665
+
4589 64 1.000
+
+
+ +
Cluster (25): 0.0725
+
5720 365 1.000
+
+
+ +
Cluster (25): 0.0826
+
5904 415 1.000
+
+
+ +
Cluster (25): 0.0852
+
5649 350 0.999
+
+
+ +
Cluster (25): 0.0966
+
5604 344 1.000
+
+
+ +
Cluster (25): 0.0990
+
5767 380 0.998
+
+
+ +
Cluster (25): 0.1060
+
5908 415 1.000
+
+
+ +
Cluster (25): 0.1165
+
4741 88 0.998
+
+
+ +
Cluster (25): 0.1240
+
5319 270 1.000
+
+
+ +
Cluster (25): 0.1290
+
5514 326 1.000
+
+
+ +
Cluster (25): 0.1353
+
5998 436 1.000
+
+
+
+
+
Identity 39 has 11
+
+
+ +
Cluster (39): 0.0542
+
5695 359 1.000
+
+
+ +
Cluster (39): 0.0763
+
5871 403 1.000
+
+
+ +
Cluster (39): 0.0782
+
4704 84 1.000
+
+
+ +
Cluster (39): 0.0878
+
5685 358 1.000
+
+
+ +
Cluster (39): 0.0884
+
5624 350 1.000
+
+
+ +
Cluster (39): 0.0907
+
5738 372 0.999
+
+
+ +
Cluster (39): 0.0974
+
5986 434 0.998
+
+
+ +
Cluster (39): 0.1087
+
5510 326 1.000
+
+
+ +
Cluster (39): 0.1107
+
4716 87 1.000
+
+
+ +
Cluster (39): 0.1191
+
5763 380 0.999
+
+
+ +
Cluster (39): 0.1266
+
4853 122 1.000
+
+
+
+
+
Identity 4 has 10
+
+
+ +
Cluster (4): 0.0530
+
4400 4 1.000
+
+
+ +
Cluster (4): 0.0676
+
5684 358 1.000
+
+
+ +
Cluster (4): 0.0741
+
5633 350 1.000
+
+
+ +
Cluster (4): 0.0899
+
5516 326 1.000
+
+
+ +
Cluster (4): 0.1047
+
5974 430 0.999
+
+
+ +
Cluster (4): 0.1134
+
5360 288 1.000
+
+
+ +
Cluster (4): 0.1197
+
5472 318 1.000
+
+
+ +
Cluster (4): 0.1252
+
5596 341 1.000
+
+
+ +
Cluster (4): 0.1264
+
5334 276 1.000
+
+
+ +
Cluster (4): 0.1379
+
5988 434 0.995
+
+
+
+
+
Identity 21 has 9
+
+
+ +
Cluster (21): 0.0771
+
5060 183 0.999
+
+
+ +
Cluster (21): 0.0786
+
4774 97 1.000
+
+
+ +
Cluster (21): 0.0799
+
5050 178 0.999
+
+
+ +
Cluster (21): 0.0901
+
4872 129 1.000
+
+
+ +
Cluster (21): 0.1040
+
4535 49 1.000
+
+
+ +
Cluster (21): 0.1145
+
4899 134 1.000
+
+
+ +
Cluster (21): 0.1174
+
6003 436 1.000
+
+
+ +
Cluster (21): 0.1197
+
4664 78 0.998
+
+
+ +
Cluster (21): 0.1230
+
4730 88 1.000
+
+
+
+
+
Identity 2 has 8
+
+
+ +
Cluster (2): 0.0589
+
5809 390 0.997
+
+
+ +
Cluster (2): 0.0619
+
4415 6 0.991
+
+
+ +
Cluster (2): 0.0687
+
4426 7 0.998
+
+
+ +
Cluster (2): 0.0788
+
5517 326 1.000
+
+
+ +
Cluster (2): 0.0887
+
4394 2 0.991
+
+
+ +
Cluster (2): 0.0898
+
5580 333 1.000
+
+
+ +
Cluster (2): 0.0906
+
4399 3 0.993
+
+
+ +
Cluster (2): 0.1065
+
5806 389 0.999
+
+
+
+
+
Identity 37 has 8
+
+
+ +
Cluster (37): 0.0829
+
4957 151 1.000
+
+
+ +
Cluster (37): 0.0901
+
5487 318 0.999
+
+
+ +
Cluster (37): 0.0938
+
4986 154 1.000
+
+
+ +
Cluster (37): 0.0962
+
4965 151 0.999
+
+
+ +
Cluster (37): 0.1070
+
4698 83 1.000
+
+
+ +
Cluster (37): 0.1237
+
4960 151 1.000
+
+
+ +
Cluster (37): 0.1371
+
6010 436 0.999
+
+
+ +
Cluster (37): 0.1394
+
5888 409 1.000
+
+
+
+
+
Identity 44 has 8
+
+
+ +
Cluster (44): 0.0593
+
4777 97 0.999
+
+
+ +
Cluster (44): 0.0758
+
4955 151 1.000
+
+
+ +
Cluster (44): 0.0839
+
5411 299 1.000
+
+
+ +
Cluster (44): 0.1078
+
4827 119 1.000
+
+
+ +
Cluster (44): 0.1105
+
5547 328 0.993
+
+
+ +
Cluster (44): 0.1160
+
5533 326 0.997
+
+
+ +
Cluster (44): 0.1211
+
5261 238 0.999
+
+
+ +
Cluster (44): 0.1221
+
5187 219 1.000
+
+
+
+
+
Identity 18 has 7
+
+
+ +
Cluster (18): 0.0886
+
4517 40 0.996
+
+
+ +
Cluster (18): 0.0977
+
4650 74 0.998
+
+
+ +
Cluster (18): 0.1014
+
6012 436 0.999
+
+
+ +
Cluster (18): 0.1025
+
5821 393 1.000
+
+
+ +
Cluster (18): 0.1186
+
4743 88 0.994
+
+
+ +
Cluster (18): 0.1278
+
5537 326 0.992
+
+
+ +
Cluster (18): 0.1284
+
5344 281 0.999
+
+
+
+
+
Identity 19 has 7
+
+
+ +
Cluster (19): 0.0676
+
4522 41 0.991
+
+
+ +
Cluster (19): 0.0895
+
5348 283 0.997
+
+
+ +
Cluster (19): 0.1043
+
4524 42 0.992
+
+
+ +
Cluster (19): 0.1100
+
5545 328 1.000
+
+
+ +
Cluster (19): 0.1103
+
5744 373 0.995
+
+
+ +
Cluster (19): 0.1251
+
4905 135 1.000
+
+
+ +
Cluster (19): 0.1281
+
4640 71 0.997
+
+
+
+
+
Identity 33 has 7
+
+
+ +
Cluster (33): 0.0684
+
4964 151 0.999
+
+
+ +
Cluster (33): 0.0852
+
4666 78 0.995
+
+
+ +
Cluster (33): 0.1046
+
5471 318 1.000
+
+
+ +
Cluster (33): 0.1086
+
5462 316 0.997
+
+
+ +
Cluster (33): 0.1128
+
4671 79 1.000
+
+
+ +
Cluster (33): 0.1169
+
4681 79 0.994
+
+
+ +
Cluster (33): 0.1340
+
5040 173 0.995
+
+
+
+
+
Identity 38 has 7
+
+
+ +
Cluster (38): 0.0573
+
5035 173 0.999
+
+
+ +
Cluster (38): 0.0596
+
5905 415 1.000
+
+
+ +
Cluster (38): 0.0708
+
5075 186 0.996
+
+
+ +
Cluster (38): 0.0759
+
5842 398 0.999
+
+
+ +
Cluster (38): 0.0816
+
5115 199 0.995
+
+
+ +
Cluster (38): 0.0935
+
4703 84 1.000
+
+
+ +
Cluster (38): 0.1182
+
5282 245 1.000
+
+
+
+
+
Identity 69 has 7
+
+
+ +
Cluster (69): 0.0651
+
5180 217 1.000
+
+
+ +
Cluster (69): 0.0720
+
5836 396 0.995
+
+
+ +
Cluster (69): 0.0726
+
5248 234 0.996
+
+
+ +
Cluster (69): 0.0929
+
5861 401 0.998
+
+
+ +
Cluster (69): 0.1048
+
5151 208 0.999
+
+
+ +
Cluster (69): 0.1102
+
5872 403 1.000
+
+
+ +
Cluster (69): 0.1296
+
4868 126 0.998
+
+
+
+
+
Identity 13 has 6
+
+
+ +
Cluster (13): 0.0593
+
4472 25 1.000
+
+
+ +
Cluster (13): 0.1049
+
5965 426 0.995
+
+
+ +
Cluster (13): 0.1068
+
4801 111 0.990
+
+
+ +
Cluster (13): 0.1079
+
4572 57 0.999
+
+
+ +
Cluster (13): 0.1180
+
4746 88 0.991
+
+
+ +
Cluster (13): 0.1302
+
5661 350 0.995
+
+
+
+
+
Identity 30 has 6
+
+
+ +
Cluster (30): 0.0599
+
4648 74 1.000
+
+
+ +
Cluster (30): 0.0966
+
4665 78 0.998
+
+
+ +
Cluster (30): 0.0991
+
5416 301 1.000
+
+
+ +
Cluster (30): 0.1104
+
5474 318 1.000
+
+
+ +
Cluster (30): 0.1274
+
5410 299 1.000
+
+
+ +
Cluster (30): 0.1353
+
5956 423 1.000
+
+
+
+
+
Identity 42 has 6
+
+
+ +
Cluster (42): 0.0765
+
5911 415 0.999
+
+
+ +
Cluster (42): 0.0826
+
4726 88 1.000
+
+
+ +
Cluster (42): 0.0948
+
4738 88 0.999
+
+
+ +
Cluster (42): 0.1069
+
4728 88 1.000
+
+
+ +
Cluster (42): 0.1147
+
6004 436 1.000
+
+
+ +
Cluster (42): 0.1317
+
5512 326 1.000
+
+
+
+
+
Identity 53 has 6
+
+
+ +
Cluster (53): 0.0848
+
4908 136 1.000
+
+
+ +
Cluster (53): 0.0956
+
5910 415 0.999
+
+
+ +
Cluster (53): 0.1012
+
5476 318 1.000
+
+
+ +
Cluster (53): 0.1021
+
5198 219 0.999
+
+
+ +
Cluster (53): 0.1277
+
5147 208 1.000
+
+
+ +
Cluster (53): 0.1321
+
5370 294 1.000
+
+
+
+
+
Identity 58 has 6
+
+
+ +
Cluster (58): 0.0689
+
4945 149 1.000
+
+
+ +
Cluster (58): 0.0831
+
5083 189 1.000
+
+
+ +
Cluster (58): 0.1018
+
5202 219 0.998
+
+
+ +
Cluster (58): 0.1143
+
4963 151 0.999
+
+
+ +
Cluster (58): 0.1162
+
4936 144 0.999
+
+
+ +
Cluster (58): 0.1261
+
4881 131 1.000
+
+
+
+
+
Identity 66 has 6
+
+
+ +
Cluster (66): 0.0570
+
5088 190 0.999
+
+
+ +
Cluster (66): 0.0951
+
4850 122 1.000
+
+
+ +
Cluster (66): 0.1014
+
5106 196 1.000
+
+
+ +
Cluster (66): 0.1029
+
5185 219 1.000
+
+
+ +
Cluster (66): 0.1168
+
5090 191 1.000
+
+
+ +
Cluster (66): 0.1211
+
5137 205 1.000
+
+
+
+
+
Identity 70 has 6
+
+
+ +
Cluster (70): 0.0718
+
5181 217 0.999
+
+
+ +
Cluster (70): 0.0853
+
5225 227 1.000
+
+
+ +
Cluster (70): 0.1012
+
5846 399 1.000
+
+
+ +
Cluster (70): 0.1027
+
4866 126 0.999
+
+
+ +
Cluster (70): 0.1060
+
5216 224 1.000
+
+
+ +
Cluster (70): 0.1202
+
5696 359 1.000
+
+
+
+
+
Identity 16 has 5
+
+
+ +
Cluster (16): 0.0855
+
5756 379 1.000
+
+
+ +
Cluster (16): 0.0896
+
4745 88 0.992
+
+
+ +
Cluster (16): 0.0897
+
4651 74 0.998
+
+
+ +
Cluster (16): 0.1061
+
4481 25 0.992
+
+
+ +
Cluster (16): 0.1263
+
4608 68 0.998
+
+
+
+
+
Identity 17 has 5
+
+
+ +
Cluster (17): 0.0797
+
5460 315 0.999
+
+
+ +
Cluster (17): 0.0870
+
5442 309 1.000
+
+
+ +
Cluster (17): 0.0918
+
4489 30 0.998
+
+
+ +
Cluster (17): 0.1008
+
4486 28 0.996
+
+
+ +
Cluster (17): 0.1271
+
4491 32 0.999
+
+
+
+
+
Identity 32 has 5
+
+
+ +
Cluster (32): 0.0677
+
4663 78 0.999
+
+
+ +
Cluster (32): 0.1061
+
5825 393 0.991
+
+
+ +
Cluster (32): 0.1255
+
5002 158 0.999
+
+
+ +
Cluster (32): 0.1273
+
4900 134 1.000
+
+
+ +
Cluster (32): 0.1298
+
5959 423 0.996
+
+
+
+
+
Identity 62 has 5
+
+
+ +
Cluster (62): 0.0711
+
4993 156 1.000
+
+
+ +
Cluster (62): 0.0767
+
5061 184 1.000
+
+
+ +
Cluster (62): 0.0849
+
5322 270 0.998
+
+
+ +
Cluster (62): 0.1083
+
4848 122 1.000
+
+
+ +
Cluster (62): 0.1324
+
5031 173 1.000
+
+
+
+
+
Identity 79 has 5
+
+
+ +
Cluster (79): 0.0596
+
5483 318 1.000
+
+
+ +
Cluster (79): 0.0640
+
5561 331 1.000
+
+
+ +
Cluster (79): 0.1118
+
5803 389 0.999
+
+
+ +
Cluster (79): 0.1177
+
5520 326 1.000
+
+
+ +
Cluster (79): 0.1359
+
6009 436 0.999
+
+
+
+
+
Identity 80 has 5
+
+
+ +
Cluster (80): 0.0719
+
5954 422 0.993
+
+
+ +
Cluster (80): 0.0735
+
5497 319 0.999
+
+
+ +
Cluster (80): 0.0845
+
5961 424 0.990
+
+
+ +
Cluster (80): 0.1089
+
5678 357 1.000
+
+
+ +
Cluster (80): 0.1125
+
5609 344 1.000
+
+
+
+
+
Identity 14 has 4
+
+
+ +
Cluster (14): 0.0805
+
4909 136 1.000
+
+
+ +
Cluster (14): 0.0865
+
4473 25 0.999
+
+
+ +
Cluster (14): 0.1032
+
5034 173 1.000
+
+
+ +
Cluster (14): 0.1039
+
5341 281 1.000
+
+
+
+
+
Identity 23 has 4
+
+
+ +
Cluster (23): 0.0403
+
4595 65 0.993
+
+
+ +
Cluster (23): 0.0732
+
5650 350 0.999
+
+
+ +
Cluster (23): 0.0769
+
5927 417 0.997
+
+
+ +
Cluster (23): 0.0808
+
4543 53 1.000
+
+
+
+
+
Identity 31 has 4
+
+
+ +
Cluster (31): 0.0664
+
5488 318 0.999
+
+
+ +
Cluster (31): 0.0940
+
5706 360 0.999
+
+
+ +
Cluster (31): 0.0986
+
4652 74 0.997
+
+
+ +
Cluster (31): 0.1123
+
5920 415 0.996
+
+
+
+
+
Identity 35 has 4
+
+
+ +
Cluster (35): 0.0781
+
4678 79 0.997
+
+
+ +
Cluster (35): 0.0866
+
4682 79 0.994
+
+
+ +
Cluster (35): 0.0890
+
4676 79 0.998
+
+
+ +
Cluster (35): 0.1027
+
5958 423 0.999
+
+
+
+
+
Identity 41 has 4
+
+
+ +
Cluster (41): 0.0699
+
5993 436 1.000
+
+
+ +
Cluster (41): 0.0752
+
4720 88 1.000
+
+
+ +
Cluster (41): 0.0848
+
5469 318 1.000
+
+
+ +
Cluster (41): 0.1133
+
4943 149 1.000
+
+
+
+
+
Identity 43 has 4
+
+
+ +
Cluster (43): 0.0813
+
5152 208 0.999
+
+
+ +
Cluster (43): 0.0851
+
5473 318 1.000
+
+
+ +
Cluster (43): 0.0859
+
4773 97 1.000
+
+
+ +
Cluster (43): 0.1028
+
5937 419 1.000
+
+
+
+
+
Identity 45 has 4
+
+
+ +
Cluster (45): 0.0693
+
5824 393 0.997
+
+
+ +
Cluster (45): 0.0928
+
4787 103 0.998
+
+
+ +
Cluster (45): 0.1262
+
5293 252 0.999
+
+
+ +
Cluster (45): 0.1293
+
5705 360 1.000
+
+
+
+
+
Identity 46 has 4
+
+
+ +
Cluster (46): 0.0707
+
4791 108 1.000
+
+
+ +
Cluster (46): 0.0720
+
5840 398 0.999
+
+
+ +
Cluster (46): 0.0893
+
5162 210 1.000
+
+
+ +
Cluster (46): 0.1077
+
5712 362 0.997
+
+
+
+
+
Identity 49 has 4
+
+
+ +
Cluster (49): 0.0397
+
4828 119 1.000
+
+
+ +
Cluster (49): 0.0484
+
5553 331 1.000
+
+
+ +
Cluster (49): 0.0488
+
5101 194 1.000
+
+
+ +
Cluster (49): 0.0682
+
5190 219 1.000
+
+
+
+
+
Identity 54 has 4
+
+
+ +
Cluster (54): 0.0891
+
4914 136 1.000
+
+
+ +
Cluster (54): 0.0967
+
4944 149 1.000
+
+
+ +
Cluster (54): 0.1059
+
5290 252 1.000
+
+
+ +
Cluster (54): 0.1327
+
5277 243 1.000
+
+
+
+
+
Identity 55 has 4
+
+
+ +
Cluster (55): 0.0859
+
4927 138 0.998
+
+
+ +
Cluster (55): 0.0993
+
5295 254 1.000
+
+
+ +
Cluster (55): 0.1014
+
5296 254 0.999
+
+
+ +
Cluster (55): 0.1031
+
5901 415 1.000
+
+
+
+
+
Identity 56 has 4
+
+
+ +
Cluster (56): 0.0668
+
4928 138 0.997
+
+
+ +
Cluster (56): 0.1063
+
4736 88 0.999
+
+
+ +
Cluster (56): 0.1082
+
5719 365 1.000
+
+
+ +
Cluster (56): 0.1285
+
5828 394 1.000
+
+
+
+
+
Identity 60 has 4
+
+
+ +
Cluster (60): 0.0841
+
4952 150 0.999
+
+
+ +
Cluster (60): 0.1121
+
4977 151 0.993
+
+
+ +
Cluster (60): 0.1240
+
5875 404 0.999
+
+
+ +
Cluster (60): 0.1270
+
5876 404 0.999
+
+
+
+
+
Identity 65 has 4
+
+
+ +
Cluster (65): 0.0622
+
5902 415 1.000
+
+
+ +
Cluster (65): 0.0697
+
5084 189 1.000
+
+
+ +
Cluster (65): 0.1011
+
4793 108 1.000
+
+
+ +
Cluster (65): 0.1254
+
5477 318 1.000
+
+
+
+
+
Identity 67 has 4
+
+
+ +
Cluster (67): 0.0849
+
5119 201 1.000
+
+
+ +
Cluster (67): 0.0910
+
5201 219 0.999
+
+
+ +
Cluster (67): 0.0935
+
4894 131 0.998
+
+
+ +
Cluster (67): 0.1043
+
4858 122 0.998
+
+
+
+
+
Identity 71 has 4
+
+
+ +
Cluster (71): 0.0751
+
5191 219 1.000
+
+
+ +
Cluster (71): 0.0784
+
5374 294 0.997
+
+
+ +
Cluster (71): 0.1134
+
5625 350 1.000
+
+
+ +
Cluster (71): 0.1165
+
5971 428 0.995
+
+
+
+
+
Identity 3 has 3
+
+
+ +
Cluster (3): 0.0664
+
4973 151 0.995
+
+
+ +
Cluster (3): 0.0807
+
4395 2 0.990
+
+
+ +
Cluster (3): 0.1070
+
5578 332 0.996
+
+
+
+
+
Identity 15 has 3
+
+
+ +
Cluster (15): 0.0624
+
5146 208 1.000
+
+
+ +
Cluster (15): 0.0880
+
4479 25 0.995
+
+
+ +
Cluster (15): 0.0887
+
5026 171 0.994
+
+
+
+
+
Identity 20 has 3
+
+
+ +
Cluster (20): 0.0739
+
4529 45 0.996
+
+
+ +
Cluster (20): 0.1031
+
5970 427 0.999
+
+
+ +
Cluster (20): 0.1054
+
5785 385 0.997
+
+
+
+
+
Identity 22 has 3
+
+
+ +
Cluster (22): 0.0675
+
4542 53 1.000
+
+
+ +
Cluster (22): 0.0701
+
5366 292 0.998
+
+
+ +
Cluster (22): 0.1156
+
5723 365 0.999
+
+
+
+
+
Identity 24 has 3
+
+
+ +
Cluster (24): 0.0639
+
4575 57 0.994
+
+
+ +
Cluster (24): 0.0769
+
4571 57 1.000
+
+
+ +
Cluster (24): 0.0815
+
4969 151 0.998
+
+
+
+
+
Identity 26 has 3
+
+
+ +
Cluster (26): 0.0463
+
4596 65 0.992
+
+
+ +
Cluster (26): 0.0644
+
5754 378 0.998
+
+
+ +
Cluster (26): 0.0820
+
4713 86 0.999
+
+
+
+
+
Identity 29 has 3
+
+
+ +
Cluster (29): 0.0621
+
5797 387 0.999
+
+
+ +
Cluster (29): 0.0699
+
4641 71 0.997
+
+
+ +
Cluster (29): 0.0854
+
5698 359 1.000
+
+
+
+
+
Identity 34 has 3
+
+
+ +
Cluster (34): 0.0599
+
5465 318 1.000
+
+
+ +
Cluster (34): 0.0835
+
4675 79 0.998
+
+
+ +
Cluster (34): 0.1049
+
6005 436 1.000
+
+
+
+
+
Identity 36 has 3
+
+
+ +
Cluster (36): 0.0717
+
4679 79 0.996
+
+
+ +
Cluster (36): 0.0872
+
5724 365 0.998
+
+
+ +
Cluster (36): 0.1232
+
5957 423 0.999
+
+
+
+
+
Identity 48 has 3
+
+
+ +
Cluster (48): 0.0584
+
4962 151 1.000
+
+
+ +
Cluster (48): 0.0777
+
4823 119 1.000
+
+
+ +
Cluster (48): 0.0834
+
5566 331 0.992
+
+
+
+
+
Identity 51 has 3
+
+
+ +
Cluster (51): 0.0563
+
4852 122 1.000
+
+
+ +
Cluster (51): 0.0906
+
5280 243 1.000
+
+
+ +
Cluster (51): 0.1199
+
5530 326 0.998
+
+
+
+
+
Identity 57 has 3
+
+
+ +
Cluster (57): 0.0595
+
4941 146 0.995
+
+
+ +
Cluster (57): 0.1122
+
5237 231 0.995
+
+
+ +
Cluster (57): 0.1209
+
5567 331 0.992
+
+
+
+
+
Identity 61 has 3
+
+
+ +
Cluster (61): 0.0546
+
4958 151 1.000
+
+
+ +
Cluster (61): 0.1060
+
5022 170 0.996
+
+
+ +
Cluster (61): 0.1103
+
4838 122 1.000
+
+
+
+
+
Identity 64 has 3
+
+
+ +
Cluster (64): 0.0607
+
5080 187 0.996
+
+
+ +
Cluster (64): 0.1009
+
5369 294 1.000
+
+
+ +
Cluster (64): 0.1014
+
4884 131 1.000
+
+
+
+
+
Identity 73 has 3
+
+
+ +
Cluster (73): 0.0770
+
5275 243 1.000
+
+
+ +
Cluster (73): 0.0848
+
5328 271 1.000
+
+
+ +
Cluster (73): 0.0991
+
5939 419 1.000
+
+
+
+
+
Identity 75 has 3
+
+
+ +
Cluster (75): 0.0636
+
5302 262 1.000
+
+
+ +
Cluster (75): 0.0696
+
5423 303 0.999
+
+
+ +
Cluster (75): 0.1260
+
6011 436 0.999
+
+
+
+
+
Identity 76 has 3
+
+
+ +
Cluster (76): 0.0782
+
5949 420 0.994
+
+
+ +
Cluster (76): 0.0927
+
5345 281 0.997
+
+
+ +
Cluster (76): 0.1094
+
5891 409 0.996
+
+
+
+
+
Identity 78 has 3
+
+
+ +
Cluster (78): 0.0500
+
5338 280 1.000
+
+
+ +
Cluster (78): 0.0565
+
5481 318 1.000
+
+
+ +
Cluster (78): 0.0747
+
5919 415 0.997
+
+
+
+
+
Identity 28 has 2
+
+
+ +
Cluster (28): 0.0588
+
4639 71 0.999
+
+
+ +
Cluster (28): 0.0611
+
4635 70 0.991
+
+
+
+
+
Identity 47 has 2
+
+
+ +
Cluster (47): 0.0520
+
4655 76 0.999
+
+
+ +
Cluster (47): 0.0724
+
4799 110 0.994
+
+
+
+
+
Identity 59 has 2
+
+
+ +
Cluster (59): 0.0482
+
5365 291 0.998
+
+
+ +
Cluster (59): 0.0802
+
4947 149 0.999
+
+
+
+
+
Identity 72 has 2
+
+
+ +
Cluster (72): 0.0243
+
5235 231 0.999
+
+
+ +
Cluster (72): 0.1184
+
5632 350 1.000
+
+
+
+
+
Identity 74 has 2
+
+
+ +
Cluster (74): 0.0529
+
5300 261 0.990
+
+
+ +
Cluster (74): 0.0692
+
5765 380 0.999
+
+
+
+
+
Identity 81 has 2
+
+
+ +
Cluster (81): 0.0499
+
5506 323 0.999
+
+
+ +
Cluster (81): 0.0679
+
5670 352 0.999
+
+
+
+
+
Identity 85 has 2
+
+
+ +
Cluster (85): 0.0109
+
4583 61 0.998
+
+
+ +
Cluster (85): 0.1071
+
5672 353 0.992
+
+
+
+
+
Identity 87 has 2
+
+
+ +
Cluster (87): 0.0308
+
4869 127 0.999
+
+
+ +
Cluster (87): 0.0967
+
5682 357 0.995
+
+
+
+
+
Identity 92 has 2
+
+
+ +
Cluster (92): 0.0494
+
5886 408 0.998
+
+
+ +
Cluster (92): 0.0569
+
5847 399 0.999
+
+
+
+
+
Identity 27 has 1
+
+
+ +
Cluster (27): 0.0000
+
4622 69 0.994
+
+
+
+
+
Identity 52 has 1
+
+
+ +
Cluster (52): 0.0000
+
4878 131 1.000
+
+
+
+
+
Identity 63 has 1
+
+
+ +
Cluster (63): 0.0000
+
5005 160 0.999
+
+
+
+
+
Identity 68 has 1
+
+
+ +
Cluster (68): 0.0000
+
5121 201 0.999
+
+
+
+
+
Identity 77 has 1
+
+
+ +
Cluster (77): 0.0000
+
5466 318 1.000
+
+
+
+
+
Identity 82 has 1
+
+
+ +
Cluster (82): 0.0000
+
5554 331 1.000
+
+
+
+
+
Identity 83 has 1
+
+
+ +
Cluster (83): 0.0000
+
5644 350 0.999
+
+
+
+
+
Identity 84 has 1
+
+
+ +
Cluster (84): 0.0000
+
5668 351 0.999
+
+
+
+
+
Identity 86 has 1
+
+
+ +
Cluster (86): 0.0000
+
5673 355 1.000
+
+
+
+
+
Identity 88 has 1
+
+
+ +
Cluster (88): 0.0000
+
5736 371 0.995
+
+
+
+
+
Identity 89 has 1
+
+
+ +
Cluster (89): 0.0000
+
5813 392 1.000
+
+
+
+
+
Identity 90 has 1
+
+
+ +
Cluster (90): 0.0000
+
5815 392 0.994
+
+
+
+
+
Identity 91 has 1
+
+
+ +
Cluster (91): 0.0000
+
5823 393 0.999
+
+
+
+
+
Identity 93 has 1
+
+
+ +
Cluster (93): 0.0000
+
5890 409 0.998
+
+
+
+
+
Identity 94 has 1
+
+
+ +
Cluster (94): 0.0000
+
5955 423 1.000
+
+
+
diff --git a/ketrface/ketrface/__init__.py b/ketrface/ketrface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ketrface/ketrface/__pycache__/__init__.cpython-310.pyc b/ketrface/ketrface/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1faa59e Binary files /dev/null and b/ketrface/ketrface/__pycache__/__init__.cpython-310.pyc differ diff --git a/ketrface/ketrface/__pycache__/db.cpython-310.pyc b/ketrface/ketrface/__pycache__/db.cpython-310.pyc new file mode 100644 index 0000000..03ddd4c Binary files /dev/null and b/ketrface/ketrface/__pycache__/db.cpython-310.pyc differ diff --git a/ketrface/ketrface/__pycache__/dbscan.cpython-310.pyc b/ketrface/ketrface/__pycache__/dbscan.cpython-310.pyc new file mode 100644 index 0000000..2e9c5ce Binary files /dev/null and b/ketrface/ketrface/__pycache__/dbscan.cpython-310.pyc differ diff --git a/ketrface/ketrface/__pycache__/util.cpython-310.pyc b/ketrface/ketrface/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000..4548751 Binary files /dev/null and b/ketrface/ketrface/__pycache__/util.cpython-310.pyc differ diff --git a/ketrface/ketrface/db.py b/ketrface/ketrface/db.py new file mode 100644 index 0000000..03fe76a --- /dev/null +++ b/ketrface/ketrface/db.py @@ -0,0 +1,78 @@ +import sqlite3 +from sqlite3 import Error +import numpy as np + +sqlite3.register_adapter(np.array, lambda arr: arr.tobytes()) +sqlite3.register_converter("array", np.frombuffer) + +def create_connection(db_file): + """ create a database connection to the SQLite database + specified by db_file + :param db_file: database file + :return: Connection object or None + """ + conn = None + try: + conn = sqlite3.connect(db_file) + except Error as e: + print(e) + + return conn + +def create_face(conn, face): + """ + Create a new face in the faces table + :param conn: + :param face: + :return: face id + """ + sql = ''' + INSERT INTO faces(photoId,scanVersion,faceConfidence,top,left,bottom,right,descriptorId) + VALUES(?,?,?,?,?,?,?,?) + ''' + cur = conn.cursor() + cur.execute(sql, ( + face['photoId'], + face['scanVersion'], + face['faceConfidence'], + face['top'], + face['left'], + face['bottom'], + face['right'], + face['descriptorId'] + )) + conn.commit() + return cur.lastrowid + + +def create_face_descriptor(conn, face): + """ + Create a new face in the faces table + :param conn: + :param face: + :return: descriptor id + """ + sql = ''' + INSERT INTO facedescriptors(descriptors) + VALUES(?) + ''' + cur = conn.cursor() + cur.execute(sql, (np.array(face['vector']),)) + conn.commit() + return cur.lastrowid + +def update_face_count(conn, photoId, faces): + """ + Update the number of faces that have been matched on a photo + :param conn: + :param photoId: + :param faces: + :return: None + """ + sql = ''' + UPDATE photos SET faces=? WHERE id=? + ''' + cur = conn.cursor() + cur.execute(sql, (faces, photoId)) + conn.commit() + return None diff --git a/ketrface/ketrface/dbscan.py b/ketrface/ketrface/dbscan.py new file mode 100644 index 0000000..609660b --- /dev/null +++ b/ketrface/ketrface/dbscan.py @@ -0,0 +1,72 @@ +from ketrface.util import * + +MIN_PTS = 10 +MAX_DISTANCE = 0.25 + +Undefined = 0 +Edge = -1 +Noise = -2 + +# Union of two lists of dicts +def Union(A, B): + C = [] + for key in A + B: + if key not in C: + C.append(key) + return C + +# https://en.wikipedia.org/wiki/DBSCAN +def DBSCAN(points, eps = MAX_DISTANCE, minPts = MIN_PTS, verbose = True): + clusters = [] # Cluster list + perc = -1 + total = len(points) + for i, P in enumerate(points): + if verbose == True: + new_perc = int(100 * (i+1) / total) + if new_perc != perc: + perc = new_perc + print(f'Clustering faces {perc}% ({i}/{total} processed) complete with {len(clusters)} identities.') + + if P['cluster'] != Undefined: # Previously processed in inner loop + continue + N = RangeQuery(points, P, eps) # Find neighbors + if len(N) < minPts: # Density check + P['cluster'] = Noise # Label as Noise + continue + + C = { # Define new cluster + 'id': len(clusters), + 'faces': [ P ] + } + clusters.append(C) + + P['cluster'] = C # Label initial point + S = N # Neighbors to expand (exclude P) + S.remove(P) + + for Q in S: # Process every seed point + if Q['cluster'] == Noise: # Change Noise to border point + Q['cluster'] = C + C['faces'].append(Q) + if Q['cluster'] != Undefined: # Previously processed (border point) + continue + Q['cluster'] = C # Label neighbor + C['faces'].append(Q) + N = RangeQuery(points, Q, eps) # Find neighbors + if len(N) >= minPts: # Density check (if Q is a core point) + S = Union(S, N) # Add new neighbors to seed set + + return clusters + +def RangeQuery(points, Q, eps): + neighbors = [] + for P in points: # Scan all points in the database + if P in neighbors: + continue + distance = findCosineDistance( # Compute distance and check epsilon + Q['descriptors'], + P['descriptors']) + if distance <= eps: + neighbors += [ P ] # Add to result + return neighbors + diff --git a/ketrface/ketrface/util.py b/ketrface/ketrface/util.py new file mode 100644 index 0000000..b65e83d --- /dev/null +++ b/ketrface/ketrface/util.py @@ -0,0 +1,70 @@ +import sys +import os +import uu +from io import BytesIO +import json +import numpy as np + +original = None + +def redirect_on(file = None): + global original + if original == None: + original = sys.stdout + if file == None: + file = os.devnull + sys.stdout = open(file, 'w') + +def redirect_off(): + global original + if original != None: + sys.stdout.close() + sys.stdout = original + original = None + + +def zlib_uuencode(databytes, name=''): + ''' Compress databytes with zlib & uuencode the result ''' + inbuff = BytesIO(zlib.compress(databytes, 9)) + outbuff = BytesIO() + uu.encode(inbuff, outbuff, name=name) + return outbuff.getvalue() + +def zlib_uudecode(databytes): + ''' uudecode databytes and decompress the result with zlib ''' + inbuff = BytesIO(databytes) + outbuff = BytesIO() + uu.decode(inbuff, outbuff) + return zlib.decompress(outbuff.getvalue()) + +class NpEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + if isinstance(obj, np.floating): + return float(obj) + if isinstance(obj, np.ndarray): + return obj.tolist() + +def findCosineDistance(source_representation, test_representation): + if type(source_representation) == list: + source_representation = np.array(source_representation) + if type(test_representation) == list: + test_representation = np.array(test_representation) + a = np.matmul(np.transpose(source_representation), test_representation) + b = np.sum(np.multiply(source_representation, source_representation)) + c = np.sum(np.multiply(test_representation, test_representation)) + return 1 - (a / (np.sqrt(b) * np.sqrt(c))) + +def findEuclideanDistance(source_representation, test_representation): + if type(source_representation) == list: + source_representation = np.array(source_representation) + if type(test_representation) == list: + test_representation = np.array(test_representation) + euclidean_distance = source_representation - test_representation + euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance)) + euclidean_distance = np.sqrt(euclidean_distance) + return euclidean_distance + +def l2_normalize(x): + return x / np.sqrt(np.sum(np.multiply(x, x))) diff --git a/server/cluster.py b/server/cluster.py deleted file mode 100644 index 4d6b563..0000000 --- a/server/cluster.py +++ /dev/null @@ -1,344 +0,0 @@ -import sys -import json -import os -import piexif -import sqlite3 -from sqlite3 import Error -from PIL import Image -import numpy as np -from deepface import DeepFace -from deepface.detectors import FaceDetector - -sqlite3.register_adapter(np.array, lambda arr: arr.tobytes()) -sqlite3.register_converter("array", np.frombuffer) - -class NpEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, np.integer): - return int(obj) - if isinstance(obj, np.floating): - return float(obj) - if isinstance(obj, np.ndarray): - return obj.tolist() - -model_name = 'VGG-Face' # 'ArcFace' -detector_backend = 'mtcnn' # 'retinaface' -model = DeepFace.build_model(model_name) -face_detector = FaceDetector.build_model(detector_backend) -input_shape = DeepFace.functions.find_input_shape(model) - -def create_connection(db_file): - """ create a database connection to the SQLite database - specified by db_file - :param db_file: database file - :return: Connection object or None - """ - conn = None - try: - conn = sqlite3.connect(db_file) - except Error as e: - print(e) - - return conn - -def create_face(conn, face): - """ - Create a new face in the faces table - :param conn: - :param face: - :return: face id - """ - sql = ''' - INSERT INTO faces(photoId,scanVersion,faceConfidence,top,left,bottom,right) - VALUES(?,?,?,?,?,?,?) - ''' - cur = conn.cursor() - cur.execute(sql, ( - face['photoId'], - face['scanVersion'], - face['faceConfidence'], - face['top'], - face['left'], - face['bottom'], - face['right'] - )) - conn.commit() - return cur.lastrowid - -def create_face_descriptor(conn, faceId, descriptor): - """ - Create a new face in the faces table - :param conn: - :param faceId: - :param descriptor: - :return: descriptor id - """ - sql = ''' - INSERT INTO facedescriptors(faceId,model,descriptors) - VALUES(?,?,?) - ''' - cur = conn.cursor() - cur.execute(sql, ( - faceId, - descriptor['model'], - np.array(descriptor['descriptors']) - )) - conn.commit() - return cur.lastrowid - -def update_face_count(conn, photoId, faces): - """ - Update the number of faces that have been matched on a photo - :param conn: - :param photoId: - :param faces: - :return: None - """ - sql = ''' - UPDATE photos SET faces=? WHERE id=? - ''' - cur = conn.cursor() - cur.execute(sql, (faces, photoId)) - conn.commit() - return None - -def findCosineDistance(source_representation, test_representation): - if type(source_representation) == list: - source_representation = np.array(source_representation) - if type(test_representation) == list: - test_representation = np.array(test_representation) - a = np.matmul(np.transpose(source_representation), test_representation) - b = np.sum(np.multiply(source_representation, source_representation)) - c = np.sum(np.multiply(test_representation, test_representation)) - return 1 - (a / (np.sqrt(b) * np.sqrt(c))) - -def findEuclideanDistance(source_representation, test_representation): - if type(source_representation) == list: - source_representation = np.array(source_representation) - if type(test_representation) == list: - test_representation = np.array(test_representation) - euclidean_distance = source_representation - test_representation - euclidean_distance = np.sum(np.multiply(euclidean_distance, euclidean_distance)) - euclidean_distance = np.sqrt(euclidean_distance) - return euclidean_distance - -def l2_normalize(x): - return x / np.sqrt(np.sum(np.multiply(x, x))) - -base = '/pictures/' -conn = create_connection('../db/photos.db') -faces = [] -identities = [] - -def find_nearest_face(faces, identities, face, threshold = 0.99): - closest = None - closest_distance = -1 - for target in identities + faces: - if target == face: - continue - target_distance = findCosineDistance( - target['descriptors'], face['descriptors'] - ) - if target_distance > threshold: - continue - if closest_distance == -1 or target_distance < closest_distance: - closest = target - closest_distance = target_distance - return closest - -def merge_identities(identities, identity1, identity2): - sum1 = np.dot( - identity1['faces'], - identity1['descriptors'] - ) - sum2 = np.dot( - identity2['faces'], - identity2['descriptors'] - ) - sum = np.add(sum1, sum2) - faces = identity1['faces'] + identity2['faces'] - id = 1 - if len(identities): - id = identities[len(identities) - 1]['id'] + 1 - - return { - 'id': id, - 'descriptors': np.divide(sum, faces), - 'faces': faces - } - -def delete_identity(identities, identity): - for i, item in enumerate(identities): - if item['id'] == identity['id']: - return identities.pop(i) - return None - -def update_face_identity(identities, face, closest): - if 'identified_as' in face: - face_identity = face['identified_as'] - delete_identity(identities, face_identity) - else: - face_identity = face - - if 'identified_as' in closest: - closest_identity = closest['identified_as'] - delete_identity(identities, closest_identity) - else: - closest_identity = closest - - identity = merge_identities(identities, face_identity, closest_identity) - identities.append(identity) - closest['identified_as'] = face['identified_as'] = identity - return identity - -def cluster_faces(face): - identities = [] - perc = -1 - for i, face in enumerate(faces): - new_perc = int(100 * (i+1) / len(faces)) - if new_perc != perc: - perc = new_perc - print(f'Clustering faces {perc}% complete with {len(identities)} identities.') - closest = find_nearest_face(faces, identities, face, threshold = 0.25) - if closest == None: - continue - identity = update_face_identity(identities, face, closest) -# if identity['faces'] > 2: -# print(f'Updated identity {identity["id"]} to hold {identity["faces"]} faces.') - return identities - -def cluster_identities(identities): - perc = -1 - last_len = 0 - while last_len != len(identities): - last_len = len(identities) - for i, identity in enumerate(identities): - new_perc = int(100 * (i+1) / len(identities)) - if new_perc != perc: - perc = new_perc - print(f'Clustering identities {perc}% complete with {len(identities)} identities.') - closest = find_nearest_face([], identities, face, threshold = 0.25) - if closest == None: - continue - update_face_identity(identities, identity, closest) - return identities - -def identity_get_faces(item): - return item['faces'] - -with conn: - cur = conn.cursor() - res = cur.execute(''' - SELECT faces.id,facedescriptors.descriptors,faces.faceConfidence,faces.photoId - FROM faces - JOIN facedescriptors ON (faces.descriptorId=facedescriptors.id) - WHERE faces.identityId IS null AND faces.faceConfidence>0.99 - ''') - for row in res.fetchall(): - id, descriptors, confidence, photoId = row - face = None - for target in faces: - if target['id'] == id: - face = target - break - if face == None: - face = { - 'id': id, - } - faces.append(face) - face['faces'] = 1 - face['confidence'] = confidence - face['photoId'] = photoId - face['descriptors'] = np.frombuffer(descriptors) - - identities = cluster_faces(faces) - #identities = cluster_identities(identities) - identities.sort(reverse = True, key = identity_get_faces) - sum = 0 - for identity in identities: - sum += identity['faces'] - print(f'{identity["id"]} has {identity["faces"]} faces') - - print(f'{len(identities)} identities seeded with {sum} faces.') - - exit(0) - if False: - for key2 in faces: - if key1 == key2: - continue - face2 = faces[key2] - if face2['scanned']: - continue - face = { - 'between': (face1['id'], face2['id']), - 'confidence': (face1['confidence'], face2['confidence']) - } - - face['distanceCosine'] = findCosineDistance( - face1['descriptors'], - face2['descriptors'] - ) - face['distanceEuclidean'] = findEuclideanDistance( - face1['descriptors'], - face2['descriptors'] - ) - face['distanceEuclideanL2'] = findEuclideanDistance( - l2_normalize(face1['descriptors']), - l2_normalize(face2['descriptors']) - ) - - face['scoring'] = 0 - - if model_name == 'VGG-Face': -# thresholds = {'cosine': 0.40, 'euclidean': 0.60, 'euclidean_l2': 0.86} -# thresholds = {'cosine': 0.31, 'euclidean': 0.47, 'euclidean_l2': 0.79} - thresholds = {'cosine': 0.25, 'euclidean': 0.47, 'euclidean_l2': 0.79} - elif model_name == 'ArcFace': - thresholds = {'cosine': 0.68, 'euclidean': 4.15, 'euclidean_l2': 1.13} - - if face['distanceCosine'] < thresholds['cosine']: - face['scoring'] += 1 - if face['distanceEuclidean'] < thresholds['euclidean']: - face['scoring'] += 1 - if face['distanceEuclideanL2'] < thresholds['euclidean_l2']: - face['scoring'] += 1 - - if face['scoring'] == 3: # Same face! - if ('identity' in face1) and ('identity' in face2): - if face1['identity'] != face2['identity']: - # print(f'Identity mismatch between {key1}({face1["confidence"]}) and {key2}({face2["confidence"]})') - continue - elif 'identity' in face1: - face2['identity'] = face1['identity'] - face1['identity']['members'].append(face) - elif 'identity' in face2: - face1['identity'] = face2['identity'] - face2['identity']['members'].append(face) - else: - # print(f'Creating new identity {len(identities)} {face["between"]}') - identity = { - 'members': [], - } - face1['identity'] = face2['identity'] = identity - identity['members'].append(face) - identities.append(identity) - - for idx, identity in enumerate(identities): - count = len(identity['members']) - print('
') - print(f'
Identity {idx} has {count}
') - print('
') - for member in identity['members']: - face1 = member['between'][0] - face2 = member['between'][1] - path1 = f'faces/{"{:02d}".format(face1 % 10)}' - path2 = f'faces/{"{:02d}".format(face2 % 10)}' - print('
') - print(f'{member["confidence"][0]}') - print(f'{member["confidence"][1]}') - print('
') - print(f'
Distance: {member["distanceCosine"]}, {member["distanceEuclidean"]}, {member["distanceEuclideanL2"]}
') - print('
') - print('
') - -# update_face_count(conn, photoId, len(faces)) diff --git a/server/detect.py b/server/detect.py deleted file mode 100644 index 6a59d51..0000000 --- a/server/detect.py +++ /dev/null @@ -1,336 +0,0 @@ -import sys -import zlib -import json -import os -import piexif -import sqlite3 -from sqlite3 import Error -from PIL import Image, ImageOps -from deepface import DeepFace -from deepface.detectors import FaceDetector -from retinaface import RetinaFace -import numpy as np -import cv2 - -import uu -from io import BytesIO - -original = None - -def redirect_on(): - global original - if original == None: - original = sys.stdout - sys.stdout = open(os.devnull, 'w') - -def redirect_off(): - global original - if original != None: - sys.stdout.close() - sys.stdout = original - original = None - -def zlib_uuencode(databytes, name=''): - ''' Compress databytes with zlib & uuencode the result ''' - inbuff = BytesIO(zlib.compress(databytes, 9)) - outbuff = BytesIO() - uu.encode(inbuff, outbuff, name=name) - return outbuff.getvalue() - -def zlib_uudecode(databytes): - ''' uudecode databytes and decompress the result with zlib ''' - inbuff = BytesIO(databytes) - outbuff = BytesIO() - uu.decode(inbuff, outbuff) - return zlib.decompress(outbuff.getvalue()) - -class NpEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, np.integer): - return int(obj) - if isinstance(obj, np.floating): - return float(obj) - if isinstance(obj, np.ndarray): - return obj.tolist() - -model_name = 'VGG-Face' # 'ArcFace' -detector_backend = 'mtcnn' # 'retinaface' -model = DeepFace.build_model(model_name) -face_detector = FaceDetector.build_model(detector_backend) -input_shape = DeepFace.functions.find_input_shape(model) - - -# Adapted from DeepFace -# https://github.com/serengil/deepface/blob/master/deepface/commons/functions.py -# -# Modified to use bicubic resampling and clip expansion, as well as to -# take a PIL Image instead of numpy array -def alignment_procedure(img, left_eye, right_eye): - """ - Given left and right eye coordinates in image, rotate around point - between eyes such that eyes are horizontal - :param img: Image (not np.array) - :param left_eye: Eye appearing on the left (right eye of person) - :param right_eye: Eye appearing on the right (left eye of person) - :return: adjusted image - """ - dY = right_eye[1] - left_eye[1] - dX = right_eye[0] - left_eye[0] - radians = np.arctan2(dY, dX) - rotation = 180 + 180 * radians / np.pi - - if True: - img = img.rotate( - angle = rotation, - resample = Image.BICUBIC, - expand = True) - - return img - -def extract_faces(img, threshold=0.75, allow_upscaling = True): - if detector_backend == 'retinaface': - faces = RetinaFace.detect_faces( - img_path = img, - threshold = threshold, - model = model, - allow_upscaling = allow_upscaling) - elif detector_backend == 'mtcnn': - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # mtcnn expects RGB - - redirect_on() - res = face_detector.detect_faces(img_rgb) - redirect_off() - - faces = {} - if type(res) == list: - for i, face in enumerate(res): - x = face['box'][0] - y = face['box'][1] - w = face['box'][2] - h = face['box'][3] - faces[f'face_{i+1}'] = { # standardize properties - 'facial_area': [ x, y, x + w, y + h ], - 'landmarks': { - 'left_eye': list(face['keypoints']['left_eye']), - 'right_eye': list(face['keypoints']['right_eye']), - }, - 'score': face['confidence'], - } - - # Re-implementation of 'extract_faces' with the addition of keeping a - # copy of the face image for caching on disk - if type(faces) == dict: - for k, key in enumerate(faces): - print(f'Processing face {k+1}/{len(faces)}') - identity = faces[key] - facial_area = identity["facial_area"] - landmarks = identity["landmarks"] - left_eye = landmarks["left_eye"] - right_eye = landmarks["right_eye"] - -# markup = True - markup = False - if markup == True: # Draw the face rectangle and eyes - cv2.rectangle(img, - (int(facial_area[0]), int(facial_area[1])), - (int(facial_area[2]), int(facial_area[3])), - (0, 0, 255), 2) - cv2.circle(img, (int(left_eye[0]), int(left_eye[1])), 5, (255, 0, 0), 2) - cv2.circle(img, (int(right_eye[0]), int(right_eye[1])), 5, (0, 255, 0), 2) - - # Find center of face, then crop to square - # of equal width and height - width = facial_area[2] - facial_area[0] - height = facial_area[3] - facial_area[1] - x = facial_area[0] + width * 0.5 - y = facial_area[1] + height * 0.5 - - # Make thumbnail a square crop - if width > height: - height = width - else: - width = height - - #width *= 1.25 - #height *= 1.25 - - left = max(round(x - width * 0.5), 0) - right = min(round(left + width), img.shape[1]) # Y is 1 - top = max(round(y - height * 0.5), 0) - bottom = min(round(top + height), img.shape[0]) # X is 0 - - left_eye[0] -= top - left_eye[1] -= left - right_eye[0] -= top - right_eye[1] -= left - - facial_img = img[top: bottom, left: right] - - # Eye order is reversed as the routine does them backwards - image = Image.fromarray(facial_img) - image = alignment_procedure(image, right_eye, left_eye) - image = image.resize(size = input_shape, resample = Image.LANCZOS) - resized = np.asarray(image) - - redirect_on() - identity['vector'] = DeepFace.represent( - img_path = resized, - model_name = model_name, - model = model, # pre-built - detector_backend = detector_backend, - enforce_detection = False) - redirect_off() - - redirect_on() - identity["face"] = { - 'top': facial_area[1] / img.shape[0], - 'left': facial_area[0] / img.shape[1], - 'bottom': facial_area[3] / img.shape[0], - 'right': facial_area[2] / img.shape[1] - } - redirect_off() - - identity['image'] = Image.fromarray(resized) - - return faces - -def create_connection(db_file): - """ create a database connection to the SQLite database - specified by db_file - :param db_file: database file - :return: Connection object or None - """ - conn = None - try: - conn = sqlite3.connect(db_file) - except Error as e: - print(e) - - return conn - -def create_face(conn, face): - """ - Create a new face in the faces table - :param conn: - :param face: - :return: face id - """ - sql = ''' - INSERT INTO faces(photoId,scanVersion,faceConfidence,top,left,bottom,right,descriptorId) - VALUES(?,?,?,?,?,?,?,?) - ''' - cur = conn.cursor() - cur.execute(sql, ( - face['photoId'], - face['scanVersion'], - face['faceConfidence'], - face['top'], - face['left'], - face['bottom'], - face['right'], - face['descriptorId'] - )) - conn.commit() - return cur.lastrowid - -def create_face_descriptor(conn, face): - """ - Create a new face in the faces table - :param conn: - :param face: - :return: descriptor id - """ - sql = ''' - INSERT INTO facedescriptors(descriptors) - VALUES(?) - ''' - cur = conn.cursor() - cur.execute(sql, (np.array(face['vector']),)) - conn.commit() - return cur.lastrowid - -def update_face_count(conn, photoId, faces): - """ - Update the number of faces that have been matched on a photo - :param conn: - :param photoId: - :param faces: - :return: None - """ - sql = ''' - UPDATE photos SET faces=? WHERE id=? - ''' - cur = conn.cursor() - cur.execute(sql, (faces, photoId)) - conn.commit() - return None - -base = '/pictures/' -conn = create_connection('../db/photos.db') -with conn: - cur = conn.cursor() - res = cur.execute(''' - SELECT photos.id,photos.faces,albums.path,photos.filename FROM photos - LEFT JOIN albums ON (albums.id=photos.albumId) - WHERE photos.faces=-1 - ''') - rows = res.fetchall() - count = len(rows) - for i, row in enumerate(rows): - photoId, photoFaces, albumPath, photoFilename = row - img_path = f'{base}{albumPath}{photoFilename}' - print(f'Processing {i+1}/{count}: {img_path}') - img = Image.open(img_path) - img = ImageOps.exif_transpose(img) # auto-rotate if needed - img = img.convert() - img = np.asarray(img) - faces = extract_faces(img) - if faces is None: - print(f'Image no faces: {img_path}') - update_face_count(conn, photoId, 0) - continue - for j, key in enumerate(faces): - face = faces[key] - image = face['image'] - print(f'Writing face {j+1}/{len(faces)}') - - #face['analysis'] = DeepFace.analyze(img_path = img, actions = ['age', 'gender', 'race', 'emotion'], enforce_detection = False) - #face['analysis'] = DeepFace.analyze(img, actions = ['emotion']) - - # TODO: Add additional meta-data allowing back referencing to original - # photo - face['version'] = 1 # version 1 doesn't add much... - - data = {k: face[k] for k in set(list(face.keys())) - set(['image', 'facial_area', 'landmarks'])} - json_str = json.dumps(data, ensure_ascii=False, cls=NpEncoder) - faceDescriptorId = create_face_descriptor(conn, face) - - faceId = create_face(conn, { - 'photoId': photoId, - 'scanVersion': face['version'], - 'faceConfidence': face['score'], - 'top': face['face']['top'], - 'left': face['face']['left'], - 'bottom': face['face']['bottom'], - 'right': face['face']['right'], - 'descriptorId': faceDescriptorId, - }) - - path = f'faces/{"{:02d}".format(faceId % 10)}' - try: - os.mkdir(path) - except FileExistsError: - pass - - with open(f'{path}/{faceId}.json', 'w', encoding = 'utf-8') as f: - f.write(json_str) - - compressed_str = zlib_uuencode(json_str.encode()) - - # Encode this data into the JPG as Exif - exif_ifd = {piexif.ExifIFD.UserComment: compressed_str} - exif_dict = {"0th": {}, "Exif": exif_ifd, "1st": {}, - "thumbnail": None, "GPS": {}} - image.save(f'{path}/{faceId}.jpg', exif = piexif.dump(exif_dict)) - - update_face_count(conn, photoId, len(faces))