import numpy as np import argparse import os, sys from gui import GuiMain, GuiImage, GuiTag import cv2 import logging import magic from tmsu import * from util import * MODEL_DIMENSIONS = 224 def predict_image(model, img, top): from tensorflow.keras.applications.resnet50 import preprocess_input, decode_predictions logger = logging.getLogger(__name__) #cv2.imshow("test", img) #cv2.waitKey(0) #cv2.destroyAllWindows() array = np.expand_dims(img, axis=0) array = preprocess_input(array) predictions = model.predict(array) classes = decode_predictions(predictions, top=top) logger.debug("Predicted image classes: {}".format(classes[0])) return set([(name, prob) for _, name, prob in classes[0]]) def predict_partial(tags, model, img, x, y, rot, top): #cv2.imshow("test", img[x:(x+MODEL_DIMENSIONS), y:(y+MODEL_DIMENSIONS)]) #cv2.waitKey(0) if rot is None: tmp = img[x:(x+MODEL_DIMENSIONS), y:(y+MODEL_DIMENSIONS)] else: tmp = cv2.rotate(img[x:(x+MODEL_DIMENSIONS), y:(y+MODEL_DIMENSIONS)], rot) tags.update(predict_image(model, tmp, top)) ''' Walk over all files for the given base directory and all subdirectories recursively. Parameters: args: Argument dict. ''' def walk(args): logger = logging.getLogger(__name__) logger.info("Walking files ...") mime = magic.Magic(mime=True) files = [os.path.abspath(os.path.join(dp, f)) for dp, dn, filenames in os.walk(args["file_dir"]) for f in filenames] logger.debug("Files: {}".format(files)) logger.info("Number of files found: {}".format(len(files))) if args["index"] >= len(files): logger.error("Invalid start index. index = {}, number of files = {}".format(args["index"], len(files))) return if args["predict_images"]: from tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions from tensorflow.keras.preprocessing import image from tensorflow.keras.models import Model model = ResNet50(weights="imagenet") for i in range(args["index"], len(files)): file_path = files[i] logger.info("Handling file {}, {}".format(i, file_path)) tags = tmsu_tags(args["base"], file_path) not_empty = bool(tags) logger.info("Existing tags: {}".format(tags)) if args["open_system"]: open_system(file_path) # Detect MIME-type for file mime_type = mime.from_file(file_path) # Handle images if mime_type.split("/")[0] == "image": logger.debug("File is image") img = cv2.imread(file_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if args["predict_images"]: logger.info("Predicting image tags ...") tags_predict = set() for _ in range(4): logger.debug("Raw scan") raw = cv2.resize(img.copy(), dsize=(MODEL_DIMENSIONS, MODEL_DIMENSIONS), interpolation=cv2.INTER_CUBIC) tags_predict.update(predict_image(model, raw, args["predict_images_top"])) img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) if not args["predict_images_skip_detail"]: pool = ThreadPool(max(1, os.cpu_count() - 2), 10000) if img.shape[0] > img.shape[1]: detail = image_resize(img.copy(), height=(args["predict_images_detail_factor"] * MODEL_DIMENSIONS)) else: detail = image_resize(img.copy(), width=(args["predict_images_detail_factor"] * MODEL_DIMENSIONS)) for x in range(0, detail.shape[0], int(MODEL_DIMENSIONS/2)): for y in range(0, detail.shape[1], int(MODEL_DIMENSIONS/2)): pool.add_task(predict_partial, tags_predict, model, detail, x, y, None, args["predict_images_top"]) pool.add_task(predict_partial, tags_predict, model, detail, x, y, cv2.ROTATE_90_CLOCKWISE, args["predict_images_top"]) pool.add_task(predict_partial, tags_predict, model, detail, x, y, cv2.ROTATE_180, args["predict_images_top"]) pool.add_task(predict_partial, tags_predict, model, detail, x, y, cv2.ROTATE_90_COUNTERCLOCKWISE, args["predict_images_top"]) pool.wait_completion() tags_sorted = [tag[0] for tag in sorted(tags_predict, key=lambda tag: tag[1], reverse=True)] tags_predict = set(list(dict.fromkeys(tags_sorted))[0:args["predict_images_top"]]) logger.info("Predicted tags: {}".format(tags_predict)) tags.update(tags_predict) if args["gui_tag"]: while(True): # For GUI inputs (rotate, ...) logger.debug("Showing image GUI ...") img_show = image_resize(img, width=args["gui_image_length"]) if img.shape[1] > img.shape[0] else image_resize(img, height=args["gui_image_length"]) #img_show = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB) ret = GuiImage(i, file_path, img_show, tags).loop() tags = set(ret[1]).difference({''}) if ret[0] == GuiImage.RETURN_ROTATE_90_CLOCKWISE: img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) elif ret[0] == GuiImage.RETURN_ROTATE_90_COUNTERCLOCKWISE: img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) elif ret[0] == GuiImage.RETURN_NEXT: break elif ret[0] == GuiImage.RETURN_ABORT: return else: if args["gui_tag"]: while(True): logger.debug("Showing generic tagging GUI ...") ret = GuiTag(i, file_path, tags).loop() tags = set(ret[1]).difference({''}) if ret[0] == GuiTag.RETURN_NEXT: break elif ret[0] == GuiTag.RETURN_ABORT: return if ((not args["gui_tag"]) and (not args["skip_prompt"])): tags = set(input_with_prefill("\nTags for file {}:\n".format(file_path), ','.join(tags)).split(",")) logger.info("Tagging {}".format(tags)) tmsu_tag(args["base"], file_path, tags, untag=not_empty) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Tag multiple files using TMSU.') parser.add_argument('-b', '--base', nargs='?', default='.', type=dir_path, help='Base directory with database (default: %(default)s)') parser.add_argument('-f', '--file-dir', nargs='?', default='.', type=dir_path, help='File directory for walking (default: %(default)s)') parser.add_argument('-g', '--gui', nargs='?', const=1, default=False, type=bool, help='Show main GUI (default: %(default)s)') parser.add_argument('--predict-images', nargs='?', const=1, default=False, type=bool, help='Use prediction for image tagging (default: %(default)s)') parser.add_argument('--predict-images-top', nargs='?', const=1, default=10, type=int, help='Defines how many top prediction keywords should be used (default: %(default)s)') parser.add_argument('--predict-images-detail-factor', nargs='?', const=1, default=2, type=int, help='Width factor for detail scan, multiplied by 224 for ResNet50 (default: %(default)s)') parser.add_argument('--predict-images-skip-detail', nargs='?', const=1, default=False, type=bool, help='Skip detail scan in image prediction (default: %(default)s)') parser.add_argument('--gui-tag', nargs='?', const=1, default=False, type=bool, help='Show GUI for tagging (default: %(default)s)') parser.add_argument('--gui-image-length', nargs='?', const=1, default=800, type=int, help='Length of longest side for preview (default: %(default)s)') parser.add_argument('--open-system', nargs='?', const=1, default=False, type=bool, help='Open all files with system default (default: %(default)s)') parser.add_argument('-s', '--skip-prompt', nargs='?', const=1, default=False, type=bool, help='Skip prompt for file tags (default: %(default)s)') parser.add_argument('-i', '--index', nargs='?', const=1, default=0, type=int, help='Start tagging at the given file index (default: %(default)s)') parser.add_argument('-v', '--verbose', action="count", default=0, help="Verbosity level") args = parser.parse_args() if args.verbose == 0: log_level = logging.WARNING elif args.verbose == 1: log_level = logging.INFO elif args.verbose >= 2: log_level = logging.DEBUG logging.basicConfig(stream=sys.stdout, level=log_level) logger = logging.getLogger(__name__) args = { "base": args.base, "file_dir": args.file_dir, "gui": args.gui, "predict_images": args.predict_images, "predict_images_top": args.predict_images_top, "predict_images_detail_factor": args.predict_images_detail_factor, "predict_images_skip_detail": args.predict_images_skip_detail, "gui_tag": args.gui_tag, "gui_image_length": args.gui_image_length, "open_system": args.open_system, "skip_prompt": args.skip_prompt, "index": args.index, "verbosity": args.verbose } logger.debug("args = {}".format(args)) if args["gui"]: logger.debug("Starting main GUI ...") args = GuiMain(args).loop() if tmsu_init(args["base"]): walk(args)