Skip to content

Commit a1ff795

Browse files
committed
adding multi-threading, tolerance config
1 parent af08bcf commit a1ff795

File tree

4 files changed

+117
-68
lines changed

4 files changed

+117
-68
lines changed

plugins/starIdentifier/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Star Identifier uses [facial recognition](https://github.com/ageitgey/face_recog
99
## Requirements
1010

1111
### Python3
12+
__version: 3.10.x +__
1213

1314
#### Installing Python
1415

@@ -66,6 +67,10 @@ Star Identifier uses a tag to find images or scenes you would like identified. B
6667

6768
Since the recognition is based on a single performer image, that image needs to have a pretty clear front-facing view of the performer's face. If face_recognition fails to find a performer's face, Star Identifier will tag that performer with `star identifier performer error` by default.
6869

70+
### Star Identifier Settings
71+
72+
You can adjust the tolerance for identification here. `0.6` is default and typical, but I've found `0.5` to work well. Lower is more strict.
73+
6974
## Running
7075

7176
### Export Performers
@@ -80,4 +85,8 @@ This loads all images in the stash database tagged with `star identifier` (by de
8085

8186
### Identify Scene Screenshots
8287

83-
This loads all scene screenshots in the stash database tagged with `star identifier` (by default), compares the recognized faces to the exported face database, and then adds all potential matches to those scenes as performers.
88+
This loads the screenshot for every scene in the stash database tagged with `star identifier` (by default), compares the recognized faces to the exported face database, and then adds all potential matches to those scenes as performers.
89+
90+
## Upcoming roadmap
91+
92+
See [issues](https://github.com/axxeman23/star_identifier/issues)

plugins/starIdentifier/star_identifier.py

Lines changed: 92 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import os
77
import pathlib
8+
from concurrent.futures import ProcessPoolExecutor
89

910
# external
1011
import urllib.request
@@ -130,8 +131,6 @@ def debug_func(client):
130131
#
131132

132133
def export_known(client):
133-
# This would be faster multi-threaded, but that seems to break face_recognition
134-
135134
log.LogInfo('Getting all performer images...')
136135

137136
performers = client.getPerformerImages()
@@ -151,19 +150,24 @@ def export_known(client):
151150

152151
log.LogInfo('Starting performer image export (this might take a while)')
153152

154-
for performer in performers:
155-
log.LogProgress(count / total)
153+
futures_list = []
154+
155+
with ProcessPoolExecutor(max_workers=10) as executor:
156+
for performer in performers:
157+
futures_list.append(executor.submit(encode_performer_from_url, performer))
156158

157-
image = face_recognition.load_image_file(urllib.request.urlopen(performer['image_path']))
158-
try:
159-
encoding = face_recognition.face_encodings(image)[0]
160-
outputDict[performer['id']] = encoding
161-
except IndexError:
162-
log.LogInfo(f"No face found for {performer['name']}")
163-
errorList.append(performer)
159+
for future in futures_list:
160+
log.LogProgress(count / total)
164161

165-
count += 1
162+
try:
163+
result = future.result()
164+
outputDict[result['id']] = result['encodings']
165+
except IndexError:
166+
log.LogInfo(f"No face found for {result['name']}")
167+
errorList.append({ 'id': result['id'], 'name': result['name'] })
166168

169+
count += 1
170+
167171
np.savez(encodings_path, **outputDict)
168172
json_print(errorList, errors_path)
169173

@@ -179,33 +183,75 @@ def export_known(client):
179183
# Facial recognition functions
180184
#
181185

186+
# Encoding
187+
188+
def encode_performer_from_url(performer):
189+
image = face_recognition.load_image_file(urllib.request.urlopen(performer['image_path']))
190+
performer['encodings'] = face_recognition.face_encodings(image)[0]
191+
return performer
192+
193+
194+
# Matching
195+
196+
def get_recognized_ids_from_image(image, known_face_encodings, ids):
197+
image['matched_ids'] = get_recognized_ids(face_recognition.load_image_file(image['path']), known_face_encodings, ids)
198+
199+
return image
182200

183-
def get_recognized_ids_from_path(image_path, known_face_encodings, ids):
184-
return get_recognized_ids(face_recognition.load_image_file(image_path), known_face_encodings, ids)
201+
def get_recognized_ids_from_scene_screenshot(scene, known_face_encodings, ids):
202+
image = urllib.request.urlopen(scene['paths']['screenshot'])
203+
scene['matched_ids'] = get_recognized_ids(face_recognition.load_image_file(image), known_face_encodings, ids)
185204

186-
def get_recognized_ids_from_url(image_url, known_face_encodings, ids):
187-
image = urllib.request.urlopen(image_url)
188-
return get_recognized_ids(face_recognition.load_image_file(image), known_face_encodings, ids)
205+
return scene
189206

190207
def get_recognized_ids(image_file, known_face_encodings, ids):
191208
unknown_face_encodings = face_recognition.face_encodings(image_file)
192209

193210
recognized_ids = np.empty((0,0), int)
194211

195212
for unknown_face in unknown_face_encodings:
196-
results = face_recognition.compare_faces(known_face_encodings, unknown_face)
213+
results = face_recognition.compare_faces(known_face_encodings, unknown_face, tolerance=config.tolerance)
197214

198215
recognized_ids = np.append(recognized_ids, [ids[i] for i in range(len(results)) if results[i] == True])
199216

200217
return np.unique(recognized_ids).tolist()
201218

219+
# Execution
220+
221+
def execute_identification_list(known_face_encodings, ids, args):
222+
count = 0
223+
futures_list = []
224+
225+
with ProcessPoolExecutor(max_workers=10) as executor:
226+
for item in args['items']:
227+
futures_list.append(executor.submit(args['executor_func'], *[item, known_face_encodings, ids]))
228+
229+
for future in futures_list:
230+
log.LogProgress(count / args['total'])
231+
232+
debug_print(future)
233+
234+
try:
235+
result = future.result()
236+
237+
if not len(result['matched_ids']):
238+
log.LogInfo(f"No matching performer found for {args['name']} id {result['id']}. Moving on to next {args['name']}...")
239+
else:
240+
log.LogDebug(f"updating {args['name']} {result['id']} with ")
241+
args['submit_func'](result['id'], result['matched_ids'])
242+
except IndexError:
243+
log.LogError(f"No face found in tagged {args['name']} id {result['id']}. Moving on to next {args['name']}...")
244+
except:
245+
log.LogError(f"Unknown error comparing tagged {args['name']} id {result['id']}. Moving on to next {args['name']}...")
246+
247+
count += 1
248+
202249
# Imgs
203250

204251
def identify_imgs(client, ids, known_face_encodings):
205252
log.LogInfo(f"Getting images tagged with '{config.tag_name_identify}'...")
206253

207254
images = client.findImages(get_scrape_tag_filter(client))
208-
count = 0
209255
total = len(images)
210256

211257
if not total:
@@ -214,28 +260,19 @@ def identify_imgs(client, ids, known_face_encodings):
214260

215261
log.LogInfo(f"Found {total} tagged images. Starting identification...")
216262

217-
for image in images:
218-
log.LogProgress(count / total)
219-
220-
try:
221-
matching_performer_ids = get_recognized_ids_from_path(image['path'], known_face_encodings, ids)
222-
except IndexError:
223-
log.LogError(f"No face found in tagged image id {image['id']}. Moving on to next image...")
224-
continue
225-
except:
226-
log.LogError(f"Unknown error comparing tagged image id {image['id']}. Moving on to next image...")
227-
continue
228-
229-
if not len(matching_performer_ids):
230-
log.LogInfo(f"No matching performer found for image id {image['id']}. Moving on to next image...")
231-
continue
232-
233-
client.updateImage({
234-
'id': image['id'],
235-
'performer_ids': matching_performer_ids
236-
})
263+
execution_args = {
264+
'name': 'image',
265+
'items': images,
266+
'total': total,
267+
'executor_func': get_recognized_ids_from_image,
268+
'submit_func': client.addPerformersToImage
269+
}
237270

238-
count += 1
271+
execute_identification_list(
272+
known_face_encodings,
273+
ids,
274+
execution_args
275+
)
239276

240277
log.LogInfo('Image identification complete!')
241278

@@ -245,7 +282,6 @@ def identify_scene_screenshots(client, ids, known_face_encodings):
245282
log.LogInfo(f"Getting scenes tagged with '{config.tag_name_identify}'...")
246283

247284
scenes = client.getScenePaths(get_scrape_tag_filter(client))
248-
count = 0
249285
total = len(scenes)
250286

251287
if not total:
@@ -254,34 +290,24 @@ def identify_scene_screenshots(client, ids, known_face_encodings):
254290

255291
log.LogInfo(f"Found {total} tagged scenes. Starting identification...")
256292

257-
for scene in scenes:
258-
log.LogProgress(count / total)
259-
260-
matching_performer_ids = np.empty((0,0), int)
261-
screenshot = scene['paths']['screenshot']
262-
263-
try:
264-
matches = get_recognized_ids_from_url(screenshot, known_face_encodings, ids)
265-
log.LogInfo(f"{len(matches)} performers identified in scene id {scene['id']}'s screenshot")
266-
matching_performer_ids = np.append(matching_performer_ids, matches)
267-
except IndexError:
268-
log.LogError(f"No face found in screenshot for scene id {scene['id']}. Moving on to next image...")
269-
continue
270-
except Exception as error:
271-
log.LogError(f"Error type = {type(error).__name__} comparing screenshot for scene id {scene['id']}. Moving on to next image...")
272-
continue
273-
274-
matching_performer_ids = np.unique(matching_performer_ids).tolist()
275-
276-
log.LogDebug(f"Found performers in scene id {scene['id']} : {matching_performer_ids}")
293+
execution_args = {
294+
'name': 'scene',
295+
'items': scenes,
296+
'total': total,
297+
'executor_func': get_recognized_ids_from_scene_screenshot,
298+
'submit_func': client.addPerformersToScene
299+
}
277300

278-
client.addPerformersToScene(scene['id'], matching_performer_ids)
301+
execute_identification_list(
302+
known_face_encodings,
303+
ids,
304+
execution_args
305+
)
279306

280-
count += 1
281-
282-
log.LogInfo("Screenshot identification complete!")
307+
log.LogInfo("Scene screenshot identification complete!")
283308

284-
main()
309+
if __name__ == "__main__":
310+
main()
285311

286312

287313
# https://github.com/ageitgey/face_recognition

plugins/starIdentifier/star_identifier_config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@
1616

1717
# If the identifier can't find a face for a performer,
1818
# it will add this tag to that performer
19-
tag_name_encoding_error = 'star identifier performer error'
19+
tag_name_encoding_error = 'star identifier performer error'
20+
21+
#
22+
# Star Identifier Settings
23+
#
24+
25+
# Tolerance: How much distance between faces to consider it a match.
26+
# Lower is more strict. 0.6 is typical best performance.
27+
tolerance = 0.6

plugins/starIdentifier/star_identifier_interface.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ def updateImage(self, image_data):
257257

258258
self.__callGraphQL(query, variables)
259259

260+
def addPerformersToImage(self, image_id, performer_ids):
261+
self.updateImage({
262+
'id': image_id,
263+
'performer_ids': performer_ids
264+
})
265+
260266
def bulkPerformerAddTags(self, performer_ids, tag_ids):
261267
query = """
262268
mutation($ids: [ID!], $tag_ids: BulkUpdateIds) {

0 commit comments

Comments
 (0)