Skip to content

Commit

Permalink
add: [core] Adding BOW ORB - Also use as an example commit of "how to…
Browse files Browse the repository at this point in the history
… add an algorithm to the library". Very very useful and important commit ! :)
  • Loading branch information
Vincent-CIRCL committed Aug 7, 2019
1 parent 86da5c9 commit 9942a00
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 19 deletions.
10 changes: 10 additions & 0 deletions carlhauser_server/Configuration/distance_engine_conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from collections import namedtuple

from common.environment_variable import JSON_parsable_Dict
from common.environment_variable import JSON_parsable_Enum
from enum import Enum, auto


class BOW_CMP_HIST(JSON_parsable_Enum, Enum):
CORREL = auto() # Standard
BHATTACHARYYA = auto()


class Default_distance_engine_conf(JSON_parsable_Dict):
def __init__(self):
Expand All @@ -16,6 +24,8 @@ def __init__(self):
# ORB PARAMETERS
self.CROSSCHECK: bool = True

# Bow_ORB PARAMETERS
self.BOW_CMP_HIST = BOW_CMP_HIST.CORREL.name


def parse_from_dict(conf):
Expand Down
17 changes: 11 additions & 6 deletions carlhauser_server/Configuration/feature_extractor_conf.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from collections import namedtuple
from enum import Enum, auto
from typing import List
import pathlib

from carlhauser_server.Configuration.algo_conf import Algo_conf
from common.environment_variable import JSON_parsable_Enum, JSON_parsable_Dict

from common.environment_variable import get_homedir

class Distance_MergingMethod(JSON_parsable_Enum, Enum):
MAX = auto()
Expand Down Expand Up @@ -34,23 +35,27 @@ def __init__(self):

# HASH parameters
self.A_HASH: Algo_conf = Algo_conf("A_HASH", False, 0.02, 0.08, distance_weight=1)
self.P_HASH: Algo_conf = Algo_conf("P_HASH", True, 0.08, 0.08, distance_weight=1)
self.P_HASH: Algo_conf = Algo_conf("P_HASH", False, 0.08, 0.08, distance_weight=1) # True
self.P_HASH_SIMPLE: Algo_conf = Algo_conf("P_HASH_SIMPLE", False, 0.04, 0.06, distance_weight=1)
self.D_HASH: Algo_conf = Algo_conf("D_HASH", True, 0.04, 0.08, distance_weight=1)
self.D_HASH: Algo_conf = Algo_conf("D_HASH", False, 0.04, 0.08, distance_weight=1) # True
self.D_HASH_VERTICAL: Algo_conf = Algo_conf("D_HASH_VERTICAL", False, 0.04, 0.04, distance_weight=1)
self.W_HASH: Algo_conf = Algo_conf("W_HASH", False, 0.06, 0.08, distance_weight=1)
self.TLSH: Algo_conf = Algo_conf("TLSH", True, 0.16, 0.18, distance_weight=1)
self.TLSH: Algo_conf = Algo_conf("TLSH", False, 0.16, 0.18, distance_weight=1) # True

# Visual Descriptors parameters
self.ORB: Algo_conf = Algo_conf("ORB", True, 0.0, 0.2, distance_weight=5)
self.ORB: Algo_conf = Algo_conf("ORB", False, 0.0, 0.2, distance_weight=5) # True
self.ORB_KEYPOINTS_NB: int = 500

self.BOW_ORB : Algo_conf = Algo_conf("BOW_ORB", True, 0.08, 0.32, distance_weight=5) # True
self.BOW_VOCAB_PATH : pathlib.Path = get_homedir() / "vocab.npy"

# Algo list # /! IMPORTANT !\ BE-AWARE THAT /! IMPORTANT !\
# IF YOU MODIFY PROGRAMMATICALLY ONE ELEMENT LATER, YOU NEED TO CHANGE IT IN THIS LIST TOO !
self.list_algos: List[Algo_conf] = [self.A_HASH, self.P_HASH, self.P_HASH_SIMPLE,
self.D_HASH, self.D_HASH_VERTICAL, self.W_HASH,
self.TLSH,
self.ORB]
self.ORB,
self.BOW_ORB]

# Merging method
self.DISTANCE_MERGING_METHOD: Distance_MergingMethod = Distance_MergingMethod.WEIGHTED_MEAN.name
Expand Down
108 changes: 108 additions & 0 deletions carlhauser_server/DistanceEngine/distance_bow_orb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import logging
import traceback
from typing import Dict, List

import cv2

import carlhauser_server.Configuration.database_conf as database_conf
import carlhauser_server.Configuration.distance_engine_conf as distance_engine_conf
import carlhauser_server.Configuration.feature_extractor_conf as feature_extractor_conf
import carlhauser_server.DistanceEngine.scoring_datastrutures as sd
from carlhauser_server.Configuration.algo_conf import Algo_conf
from common.CustomException import AlgoFeatureNotPresentError
from carlhauser_server.DistanceEngine.distance_hash import Distance_Hash as dist_hash


class Distance_BoW_ORB:
def __init__(self, db_conf: database_conf.Default_database_conf, dist_conf: distance_engine_conf.Default_distance_engine_conf, fe_conf: feature_extractor_conf.Default_feature_extractor_conf):
# STD attributes
self.logger = logging.getLogger(__name__)
self.logger.info("Creation of a Distance ORB Engine")

# Save configuration
self.db_conf = db_conf # TODO : REMOVE = NOT USEFUL FOR NOW !
self.dist_conf = dist_conf
self.fe_conf = fe_conf

# TODO :
self.orb_matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=dist_conf.CROSSCHECK)

def bow_orb_distance(self, pic_package_from: Dict, pic_package_to: Dict) -> Dict[str, sd.AlgoMatch]:
"""
Distance between two provided pictures (dicts) with BoW-ORB methods
:param pic_package_from: first picture dict
:param pic_package_to: second picture dict
:return: A dictionary of algo name to the match detail (distance, decision ..)
"""

answer = {}
self.logger.info("BoW-Orb distance computation ... ")

# Verify if what is needed to compute it is present
if pic_package_from.get("BOW_ORB_DESCRIPTOR", None) is None \
or pic_package_to.get("BOW_ORB_DESCRIPTOR", None) is None:
self.logger.warning(f"BoW-ORB descriptors are NOT presents in the results.")
raise AlgoFeatureNotPresentError("None BoW-ORB descriptors in orb distance.")

# Add result for enabled algorithms
try:
if self.fe_conf.BOW_ORB.get("is_enabled", False):
answer = self.add_results(self.fe_conf.ORB, pic_package_from, pic_package_to, answer)

except Exception as e:
self.logger.error(traceback.print_tb(e.__traceback__))
self.logger.error("Error during bow-orb distance calculation : " + str(e))

return answer

def add_results(self, algo_conf: Algo_conf, pic_package_from: Dict, pic_package_to: Dict, answer: Dict) -> Dict:
"""
Add results to answer dict, depending on the algorithm name we want to compute
Ex : Input {} -> Output {"BOW_ORB":{"name":"BOW_ORB", "distance":0.3,"decision":YES}}
:param algo_conf: An algorithm configuration (to specify which algorithm to launch)
:param pic_package_from: first picture dict
:param pic_package_to: second picture dict
:param answer: Current dict of algo_name to algo match (will be updated and returned)
:return: a dict of algo_name to algo match
"""

algo_name = algo_conf.get('algo_name')

# Depending on the type of
self.logger.debug(f"Comparison for BOW : {self.dist_conf.BOW_CMP_HIST} of {type(self.dist_conf.BOW_CMP_HIST)} "
f"and {distance_engine_conf.BOW_CMP_HIST.CORREL.name} of {type(distance_engine_conf.BOW_CMP_HIST.CORREL.name)}")

if self.dist_conf.BOW_CMP_HIST == distance_engine_conf.BOW_CMP_HIST.CORREL.name:
tmp_dist = 1 - cv2.compareHist(pic_package_from["BOW_ORB_DESCRIPTOR"],
pic_package_to["BOW_ORB_DESCRIPTOR"],
cv2.HISTCMP_CORREL)
elif self.dist_conf.BOW_CMP_HIST == distance_engine_conf.BOW_CMP_HIST.BHATTACHARYYA.name:
tmp_dist = cv2.compareHist(pic_package_from["BOW_ORB_DESCRIPTOR"],
pic_package_to["BOW_ORB_DESCRIPTOR"],
cv2.HISTCMP_BHATTACHARYYA)
else:
raise Exception('BOW ORB : HISTOGRAM COMPARISON MODE INCORRECT')

# Add the distance as an AlgoMatch
answer[algo_name] = sd.AlgoMatch(name=algo_name,
distance=tmp_dist,
decision=self.compute_decision_from_distance(algo_conf, tmp_dist))
return answer

# ==================== ------ DECISIONS ------- ====================

@staticmethod
def compute_decision_from_distance(algo_conf: Algo_conf, dist: float) -> sd.DecisionTypes:
"""
From a distance between orb distance, gives a decision : is it a match or not ? Or maybe ?
# TODO : Evolve to more complex calculation if needed for ORB !
:param algo_conf: An algorithm configuration (to specify which algorithm to launch)
:param dist: a distance between two pictures
:return: a decision (YES,MAYBE,NO)
"""

return dist_hash.compute_decision_from_distance(algo_conf, dist)
22 changes: 16 additions & 6 deletions carlhauser_server/DistanceEngine/distance_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import carlhauser_server.DatabaseAccessor.database_worker as database_worker
import carlhauser_server.DistanceEngine.distance_hash as distance_hash
import carlhauser_server.DistanceEngine.distance_orb as distance_orb
import carlhauser_server.DistanceEngine.distance_bow_orb as distance_bow_orb
import carlhauser_server.DistanceEngine.merging_engine as merging_engine
import carlhauser_server.DistanceEngine.scoring_datastrutures as scoring_datastrutures
from common.CustomException import AlgoFeatureNotPresentError
Expand Down Expand Up @@ -41,6 +42,7 @@ def __init__(self, parent: database_worker, db_conf: database_conf.Default_datab
# Create distance extractor
self.distance_hash = distance_hash.Distance_Hash(db_conf, dist_conf, fe_conf)
self.distance_orb = distance_orb.Distance_ORB(db_conf, dist_conf, fe_conf)
self.distance_bow_orb = distance_bow_orb.Distance_BoW_ORB(db_conf, dist_conf, fe_conf)
self.merging_engine = merging_engine.Merging_Engine(db_conf, dist_conf, fe_conf)

# ==================== ------ INTER ALGO DISTANCE ------- ====================
Expand Down Expand Up @@ -70,6 +72,14 @@ def get_dist_and_decision_algos_to_algos(self, pic_package_from: Dict, pic_packa
except AlgoFeatureNotPresentError as e:
self.logger.warning(f"No feature present for orbing algorithms : {e}")

# Get BoW-ORB distances
try:
bow_orb_dict = self.distance_bow_orb.bow_orb_distance(pic_package_from, pic_package_to)
self.logger.debug(f"Computed BoW-orb distance : {bow_orb_dict}")
merged_dict.update(bow_orb_dict)
except AlgoFeatureNotPresentError as e:
self.logger.warning(f"No feature present for orbing algorithms : {e}")

self.logger.debug(f"Distance dict : {merged_dict}")
return merged_dict

Expand All @@ -84,16 +94,16 @@ def get_dist_and_decision_picture_to_picture(self, pic_package_from, pic_package
# From distance between algos, obtain the distance between pictures
merged_dict = self.get_dist_and_decision_algos_to_algos(pic_package_from, pic_package_to)

try :
try:
dist = self.merging_engine.merge_algos_distance(merged_dict)
except Exception as e :
self.logger.critical("Error during merging of distances. Default distance taken : 1.")
except Exception as e:
self.logger.critical(f"Error during merging of distances. Default distance taken : 1. Error: {e}")
dist = 1

try:
decision = self.merging_engine.merge_algos_decision(merged_dict)
except Exception as e :
self.logger.critical("Error during merging of decisions. Default decision taken : MAYBE.")
except Exception as e:
self.logger.critical(f"Error during merging of decisions. Default decision taken : MAYBE. Error: {e}")
decision = scoring_datastrutures.DecisionTypes.MAYBE

return dist, decision
Expand All @@ -113,7 +123,7 @@ def match_enough(self, matching_picture: scoring_datastrutures.ImageMatch) -> bo
return True

# TODO : Check for decisions
if matching_picture.decision == dt.YES.name or matching_picture.decision == dt.MAYBE.name :
if matching_picture.decision == dt.YES.name or matching_picture.decision == dt.MAYBE.name:
return True

# Picture is too "far"
Expand Down
35 changes: 28 additions & 7 deletions carlhauser_server/FeatureExtractor/feature_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

import argparse
from typing import Dict

import traceback
import carlhauser_server.Configuration.database_conf as database_conf
import carlhauser_server.Configuration.feature_extractor_conf as feature_extractor_conf
import carlhauser_server.DatabaseAccessor.database_worker as database_accessor
import carlhauser_server.FeatureExtractor.picture_hasher as picture_hasher
import carlhauser_server.FeatureExtractor.picture_orber as picture_orber
import carlhauser_server.FeatureExtractor.picture_bow_orber as picture_bow_orber
from carlhauser_server.Helpers import arg_parser
from common.environment_variable import load_server_logging_conf_file, make_small_line, QueueNames

Expand All @@ -29,6 +30,7 @@ def __init__(self, tmp_db_conf: database_conf, tmp_fe_conf: feature_extractor_co

self.picture_hasher = picture_hasher.Picture_Hasher(tmp_fe_conf)
self.picture_orber = picture_orber.Picture_Orber(tmp_fe_conf)
self.picture_bow_orber = picture_bow_orber.Picture_BoW_Orber(tmp_fe_conf)

def fetch_from_queue(self) -> (str, Dict):
"""
Expand All @@ -50,19 +52,38 @@ def process_fetched_data(self, fetched_id, fetched_dict):
picture = fetched_dict[b"img"]
except Exception as e :
self.logger.critical(f"Error while fetching dictionnary : {e} with {fetched_dict}")
raise Exception(f"Impossible to fetch image in stored dictionnary in feature worker : {e}")
raise Exception(f"Impossible to fetch image in stored dictionary in feature worker : {e}")
self.logger.info(f"Loaded picture {type(picture)}")

# Get hash values of picture
hash_dict = self.picture_hasher.hash_picture(picture)
self.logger.debug(f"Computed hashes : {hash_dict}")
try :
hash_dict = self.picture_hasher.hash_picture(picture)
self.logger.debug(f"Computed hashes : {hash_dict}")
except Exception as e :
traceback.print_tb(e.__traceback__)
self.logger.error(f"Error while computing hash dictionnary : {e}")
hash_dict = {}

# Get ORB values of picture
orb_dict = self.picture_orber.orb_picture(picture)
self.logger.debug(f"Computed orb values : {orb_dict}")
try:
orb_dict = self.picture_orber.orb_picture(picture)
self.logger.debug(f"Computed orb values : {orb_dict}")
except Exception as e :
traceback.print_tb(e.__traceback__)
self.logger.error(f"Error while computing orb dictionnary : {e}")
orb_dict = {}

# Get BoW-ORB values of picture
try:
bow_orb_dict = self.picture_bow_orber.bow_orb_picture(picture, orb_dict)
self.logger.debug(f"Computed bow orb values : {bow_orb_dict}")
except Exception as e :
traceback.print_tb(e.__traceback__)
self.logger.error(f"Error while computing BoW-Orb dictionnary : {e}")
bow_orb_dict = {}

# Merge dictionaries
merged_dict = {**hash_dict, **orb_dict}
merged_dict = {**hash_dict, **orb_dict, **bow_orb_dict}
self.logger.debug(f"To send to db dict : {merged_dict}")

# Remove old data and send dictionary in hashmap to redis
Expand Down
90 changes: 90 additions & 0 deletions carlhauser_server/FeatureExtractor/picture_bow_orber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


import logging
import cv2
from typing import Dict
import numpy as np
import carlhauser_server.Configuration.feature_extractor_conf as feature_extractor_conf
from common.environment_variable import load_server_logging_conf_file
from carlhauser_server.Helpers.bow_orb_vocabulary_creator import BoWOrb_Vocabulary_Creator
load_server_logging_conf_file()


class Picture_BoW_Orber:
def __init__(self, fe_conf: feature_extractor_conf.Default_feature_extractor_conf):
# STD attributes
self.fe_conf: feature_extractor_conf.Default_feature_extractor_conf = fe_conf
self.logger = logging.getLogger(__name__)
self.logger.info("Creation of a Picture BoW Orber")

self.algo = cv2.ORB_create(nfeatures=fe_conf.ORB_KEYPOINTS_NB)
# TODO : Dictionnary path / Vocabulary
self.bow_descriptor = cv2.BOWImgDescriptorExtractor(self.algo, cv2.BFMatcher(cv2.NORM_HAMMING))
vocab = BoWOrb_Vocabulary_Creator.load_vocab_from_file(fe_conf.BOW_VOCAB_PATH)
self.bow_descriptor.setVocabulary(vocab)

'''
def create_dict_from_folder(self, folder_path: pathlib.Path()):
self.bow_trainer = cv2.BOWKMeansTrainer(self.conf.BOW_SIZE)
for curr_image in picture_list:
self.bow_trainer.add(np.float32(curr_image.description))
self.vocab = self.bow_trainer.cluster().astype(picture_list[0].description.dtype)
def compute_distance():
if self.conf.BOW_CMP_HIST == configuration.BOW_CMP_HIST.CORREL:
dist = 1 - cv2.compareHist(pic1.description, pic2.description, cv2.HISTCMP_CORREL)
elif self.conf.BOW_CMP_HIST == configuration.BOW_CMP_HIST.BHATTACHARYYA:
dist = cv2.compareHist(pic1.description, pic2.description, cv2.HISTCMP_BHATTACHARYYA)
else:
raise Exception('BOW WRAPPER : HISTOGRAM COMPARISON MODE INCORRECT')
'''

def bow_orb_picture(self, curr_picture, orb_dict: Dict):
"""
BoW-Orb a picture and returns the BoW-orb value
:param curr_picture: the picture to bow-orb
:return: the BoW-orb version of the picture
"""
answer = {}
self.logger.info("BoW-Orbing picture ... ")

# Convert from cv's BRG default color order to RGB
# image = cv2.imread(str(path))
# image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
self.logger.debug(f"Original type {type(curr_picture)}")
arr = np.asarray(bytearray(curr_picture), dtype=np.uint8)
orb_pic = cv2.imdecode(arr, -1)
self.logger.debug(f"Picture converted to CV2 UMAT {type(orb_pic)}")

# Get keypoints from orb dictionnary OR compute it if not present
key_points = orb_dict.get("ORB_KEYPOINTS", None)
if key_points is None or key_points == []:
self.logger.warning(f"No Keypoints in provided ORB dictionnary.")
try:
self.logger.info(f"Computing Orb Keypoints in BoW-orber.")

# Compute keypoints by itself
key_points, _ = self.algo.detectAndCompute(orb_pic, None)

if key_points is None or key_points == []:
raise Exception("NO KEYPOINTS")

except Exception as e:
self.logger.error(f"Impossible to compute keypoints in BoW-Orber for provided picture : {e}")
raise e

if self.fe_conf.BOW_ORB.get("is_enabled", False):
try:
description = self.bow_descriptor.compute(orb_pic, key_points)
self.logger.warning(f"TYPE descriptor : {type(description)}")
answer["BOW_ORB_DESCRIPTOR"] = description
except Exception as e:
self.logger.error("Error during BoW-orbing : " + str(e))
raise e

return answer

0 comments on commit 9942a00

Please sign in to comment.