diff --git a/detector/configs/abstract.py b/detector/configs/abstract.py index 7f60523..0172c91 100644 --- a/detector/configs/abstract.py +++ b/detector/configs/abstract.py @@ -1,15 +1,13 @@ -from typing import Type - from detector.detection import PreprocessingParams, EdgeDetectorParams, SegmentDetectorParams, SegmentClustererParams, \ ClusterCleaningParams, IntersectionDetectorParams, RectangleDetectorParams from detector.utils.pvpd_base_class import PVPDBaseClass class Config(PVPDBaseClass): - preprocessing_params: Type[PreprocessingParams] = None - edge_detector_params: Type[EdgeDetectorParams] = None - segment_detector_params: Type[SegmentDetectorParams] = None - segment_clusterer_params: Type[SegmentClustererParams] = None - cluster_cleaning_params: Type[ClusterCleaningParams] = None - intersection_detector_params: Type[IntersectionDetectorParams] = None - rectangle_detector_params: Type[RectangleDetectorParams] = None + preprocessing_params: PreprocessingParams = None + edge_detector_params: EdgeDetectorParams = None + segment_detector_params: SegmentDetectorParams = None + segment_clusterer_params: SegmentClustererParams = None + cluster_cleaning_params: ClusterCleaningParams = None + intersection_detector_params: IntersectionDetectorParams = None + rectangle_detector_params: RectangleDetectorParams = None diff --git a/detector/detector.py b/detector/detector.py index da3c131..f30a954 100644 --- a/detector/detector.py +++ b/detector/detector.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime from typing import Tuple, Any, Type, List import numpy as np @@ -81,7 +82,8 @@ def detect_segments_functional(edge_image: np.ndarray, params: SegmentDetectorPa return segment_detector.segments @staticmethod - def cluster_segments_functional(segments, params: SegmentClustererParams, cleaning_params: ClusterCleaningParams) -> Any: + def cluster_segments_functional(segments, params: SegmentClustererParams, + cleaning_params: ClusterCleaningParams) -> Any: """Clusters the segments in :param:`segments` according to the parameters in :param:`param`. After that aggregate and clean cluster using :param:`cleaning_params` See Also: @@ -122,9 +124,17 @@ def detect_rectangles_functional(intersections, params: RectangleDetectorParams) @staticmethod def get_rectangles_labels(rectangles: List[np.ndarray], rectangle_labeler: Type[RectangleLabeler], - preprocessed_image: np.ndarray, label_path: str) -> Any: + thermal_image: np.ndarray, image_path: str, + preprocessed_image: np.ndarray, label_path: str, tags: dict) -> Any: """Create label files using labeler based on detected rectangles.""" - labeler = rectangle_labeler(rectangles=rectangles, preprocessed_image=preprocessed_image, label_path=label_path) + labeler = rectangle_labeler( + rectangles=rectangles, + thermal_image=thermal_image, + image_path=image_path, + preprocessed_image=preprocessed_image, + label_path=label_path, + tags=tags + ) if label_path: label_file = labeler.create_label_file() logging.info(f"Created label file: {label_file}") @@ -162,14 +172,15 @@ def process_panel(contours, contour_id, preprocessed, last_scaled_frame_rgb, con @staticmethod def main(image_path: str, config: Config, labelers: List[Type[RectangleLabeler]] = None, labels_path: str = None, silent: bool = True): - img = read_bgr_img(image_path) + start_time = datetime.now() + thermal_image = read_bgr_img(image_path) # distorted_image = img # if False: # undistorted_image = cv2.undistort(src=distorted_image) # else: # undistorted_image = distorted_image - preprocessed_image, scaled_image_rgb, mask = Detector.preprocess_image_functional(img, + preprocessed_image, scaled_image_rgb, mask = Detector.preprocess_image_functional(thermal_image, config.preprocessing_params, silent) @@ -190,8 +201,24 @@ def main(image_path: str, config: Config, labelers: List[Type[RectangleLabeler]] except Exception as e: logging.error(f"Failed to process panel for contour_id {contour_id} due to {e}") + end_time = datetime.now() + + + tags = { + 'start_time': start_time, + 'end_time': end_time, + 'detection_duration': end_time - start_time + } for labeler in labelers: - Detector.get_rectangles_labels(rectangles, labeler, preprocessed_image, labels_path) + Detector.get_rectangles_labels( + rectangles, + labeler, + thermal_image, + image_path, + preprocessed_image, + labels_path, + tags + ) if not silent: draw_rectangles(rectangles, scaled_image_rgb, "rectangles") diff --git a/detector/labelers/abstract.py b/detector/labelers/abstract.py index b0e17ec..7eb8e23 100644 --- a/detector/labelers/abstract.py +++ b/detector/labelers/abstract.py @@ -1,5 +1,4 @@ from abc import abstractmethod -from typing import List import numpy as np @@ -9,13 +8,17 @@ class RectangleLabeler(PVPDBaseClass): file_extension = None - def __init__(self, rectangles: list, preprocessed_image: np.ndarray, label_path: str): + def __init__(self, rectangles: list, thermal_image: np.ndarray, image_path: str, preprocessed_image: np.ndarray, + label_path: str, tags: dict): self.rectangles = rectangles self.preprocessed_image = preprocessed_image + self.thermal_image = thermal_image + self.image_path = image_path self.label_path = label_path self.class_id = 0 self.labels_collector = [] self.labeled = False + self.tags = tags @abstractmethod def label_image(self): diff --git a/detector/labelers/labelme.py b/detector/labelers/labelme.py new file mode 100644 index 0000000..3e8a1de --- /dev/null +++ b/detector/labelers/labelme.py @@ -0,0 +1,65 @@ +import base64 +import json +import os + +from labelme import LabelFile, __version__ +from labelme.label_file import LabelFileError + +from detector.labelers.abstract import RectangleLabeler +from detector.utils.utils import to_camel + + +class LabelMeLabeler(RectangleLabeler): + extension = 'json' + + def __save(self, filename, shapes, image_path, image_height, image_width, image_data=None, tags=None): + if image_data is not None: + image_data = base64.b64encode(image_data).decode("utf-8") + image_height, image_width = LabelFile._check_image_height_and_width( + image_data, image_height, image_width + ) + if tags is None: + tags = {} + data = dict( + version=__version__, + flags=None, + shapes=shapes, + imagePath=image_path, + imageData=image_data, + imageHeight=image_height, + imageWidth=image_width, + ) + for key, value in tags.items(): + assert key not in data + data[to_camel(key)] = value + try: + with open(filename, "w") as f: + json.dump(data, f, ensure_ascii=False, indent=2, default=str) + except Exception as e: + raise LabelFileError(e) + + @staticmethod + def __create_shape(rectangle): + return { + "label": "0", + "points": rectangle.tolist(), + "group_id": None, + "shape_type": "polygon", + "flags": {} + } + + def label_image(self): + self.labels_collector = self.rectangles + self.labeled = True + return self.rectangles + + def create_label_file(self): + if not self.labeled: + self.label_image() + root, ext = os.path.splitext(self.label_path) + file_name = f'{root}.{self.extension}' + image_data = LabelFile().load_image_file(self.image_path) + shapes = [self.__create_shape(rectangle) for rectangle in self.rectangles] + self.__save(file_name, shapes, f'{root}.JPG', self.preprocessed_image.shape[0], + self.preprocessed_image.shape[1], image_data, self.tags) + return file_name diff --git a/detector/labelers/yolo.py b/detector/labelers/yolo.py index 2701435..c1a18eb 100644 --- a/detector/labelers/yolo.py +++ b/detector/labelers/yolo.py @@ -33,7 +33,6 @@ def label_image(self): return self.labels_collector def create_label_file(self): - if not self.labeled: self.label_image() root, ext = os.path.splitext(self.label_path) diff --git a/detector/utils/utils.py b/detector/utils/utils.py index 4199da2..df2261c 100644 --- a/detector/utils/utils.py +++ b/detector/utils/utils.py @@ -1,3 +1,4 @@ +from functools import lru_cache from pathlib import Path import cv2 @@ -91,9 +92,19 @@ def auto_canny(image): return lower, upper +def to_camel(string): + return ''.join((wd.title() if i else wd) for (i, wd) in enumerate(string.split('_'))) + + +@lru_cache() +def color_maps(): + return {color_map_name: value for color_map_name, value in cm.__dict__.items() if + getattr(value, '__module__', "") == 'matplotlib.colors'} + + def available_color_maps(): - return cm._cmap_registry.keys() + return color_maps().keys() def get_color_map_by_name(name): - return cm._cmap_registry[name] + return color_maps()[name] diff --git a/detector_cli.py b/detector_cli.py index 5828ff1..b15cc3b 100755 --- a/detector_cli.py +++ b/detector_cli.py @@ -19,16 +19,36 @@ image_types = (THERMAL, RAW) +def process(multithread, **args): + if multithread: + process_multiple(**args) + else: + process_single(**args) + + +def process_single(file_paths, config, output_dir, labelers, silent): + logging.info(f"Annotation of images started. Result will be saved to '{output_dir}'.") + for index, multi_color_file_paths in enumerate(file_paths): + if multi_color_file_paths is None: + logging.warning("No thermal image provided. Cannot proceed with PV panels detection.") + for subindex, file_path in enumerate(multi_color_file_paths): + output_path = f"{output_dir}/{os.path.basename(file_path)}" + logging.info(f"Annotation of img '{file_path}' started. Result will be saved to '{output_path}'. " + f"Run index {index}.{subindex}.") + Detector.main(os.path.abspath(file_path), config, labelers, output_path, silent) + + def save_img_callback(args): - save_img(args[0], args[1]) + pass + # save_img(args[0], args[1]) def error_callback(e): logging.error(f"Annotation of img failed with: {e}") -def process(file_paths, config, output_dir, labelers, silent): - logging.info(f"Annotation of images started. Result will be saved to '{output_dir}'.") +def process_multiple(file_paths, config, output_dir, labelers, silent): + logging.info(f"Multithreading annotation of images started. Result will be saved to '{output_dir}'.") p = Pool(max(os.cpu_count() - 2, 1)) for index, multi_color_file_paths in enumerate(file_paths): if multi_color_file_paths is None: @@ -46,6 +66,7 @@ def process(file_paths, config, output_dir, labelers, silent): helps = { + 'multithread': "Multithreaded version of detection. Speed up detection using all available processor resources", 'config': "Config Class name. Based on config PV panels are detected from image. " "User can create his own config and place in detector/configs directory. " "Class name must be unique", @@ -70,7 +91,7 @@ def process(file_paths, config, output_dir, labelers, silent): def parse_arguments(): init_logger() parser = argparse.ArgumentParser(prog='PhotoVoltaic Panels Detector', description='') - + parser.add_argument('-m', '--multithread', action='store_true', help=helps['multithread']) parser.add_argument('-t', '--type', choices=image_types, default=THERMAL, help=helps['type']) extract_group = parser.add_argument_group('extract') process_group = parser.add_argument_group('process') @@ -99,14 +120,22 @@ def parse_arguments(): files = iter(files) if args.type == RAW: - thermal_files = (ThermalImageExtractor.get_thermal_image_file_path(file, args.color_map, args.thermal_image_output) for file in files) + thermal_files = ( + ThermalImageExtractor.get_thermal_image_file_path( + file, + args.color_map, + args.thermal_image_output + ) for file in files) files = thermal_files - process(file_paths=files, - config=Config.get_subclass_by_name(args.config), - silent=not args.show_step_images, - labelers=[RectangleLabeler.get_subclass_by_name(labeler) for labeler in args.labelers], - output_dir=args.output_dir) + process( + multithread=args.multithread, + file_paths=files, + config=Config.get_subclass_by_name(args.config), + silent=not args.show_step_images, + labelers=[RectangleLabeler.get_subclass_by_name(labeler) for labeler in args.labelers], + output_dir=args.output_dir + ) return args diff --git a/experiments/confusion_matrix.py b/experiments/confusion_matrix.py deleted file mode 100644 index a07ef17..0000000 --- a/experiments/confusion_matrix.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import cv2 -from sklearn.metrics import confusion_matrix -from sklearn.metrics import accuracy_score -from sklearn.metrics import classification_report - -import numpy as np - - -true_img=cv2.imread("data/small-set/label.png") -pred_img=cv2.imread("data/small-set/pred2.tiff") - -pred_img = np.array(cv2.threshold(pred_img, 1, 255, cv2.THRESH_BINARY)[1][:,:,0]).flatten() -true_img = np.array(cv2.threshold(true_img, 1, 255, cv2.THRESH_BINARY)[1][:,:,0]).flatten() - - -print("Confusion Matrix: ", - confusion_matrix(true_img, pred_img)) - -print ("Accuracy : ", - accuracy_score(true_img, pred_img)*100) - -print( - classification_report(true_img, pred_img)) - -print( - jaccard_similarity_score(true_img, pred_img)) \ No newline at end of file diff --git a/experiments/dataset_extension.py b/experiments/dataset_extension.py index 4a11ec1..d31621a 100644 --- a/experiments/dataset_extension.py +++ b/experiments/dataset_extension.py @@ -3,7 +3,7 @@ import random from shutil import copyfile -from extractor.extractor import ThermalImageExtractor +from detector.extractor.extractor import ThermalImageExtractor path = 'data/raw' # https://en.wikipedia.org/wiki/Reservoir_sampling diff --git a/experiments/remove_not_labelled.py b/experiments/remove_not_labelled.py deleted file mode 100644 index ffc3592..0000000 --- a/experiments/remove_not_labelled.py +++ /dev/null @@ -1,15 +0,0 @@ -import glob -import os - -jpg_files = glob.glob('./data/thermal-panels/*.JPG') -json_files = glob.glob('./data/thermal-panels/*.json') - -jpg_file_names = [os.path.basename(x) for x in jpg_files] -json_file_names = [os.path.basename(x).replace('.json', '') for x in json_files] - -jpg_file_names = [x for x in jpg_file_names if os.path.basename(x).replace('.JPG', '') in json_file_names] - - -for f in jpg_files: - if os.path.basename(f) not in jpg_file_names: - os.remove(f) diff --git a/experiments/requirements.txt b/experiments/requirements.txt new file mode 100644 index 0000000..bb2cad7 --- /dev/null +++ b/experiments/requirements.txt @@ -0,0 +1,10 @@ +imgviz==1.4.0 +labelme==4.5.13 +pandas==1.3.3 +PyQt5==5.15.2 +PyQt5-sip==12.9.0 +pytz==2021.3 +PyYAML==5.4.1 +QtPy==1.11.2 +Shapely==1.7.1 +termcolor==1.1.0 diff --git a/requirements.txt b/requirements.txt index 5b670f7..4dc7bed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ cycler==0.10.0 joblib==1.0.1 kiwisolver==1.3.2 -matplotlib==3.4.3 +matplotlib==3.2.2 numpy==1.21.2 opencv-python-headless==4.5.3.56 Pillow==8.3.2