def __init__(self, obj_classes=None, tag_metas=None): ''' :param obj_classes: Collection that stores ObjClass instances with unique names. :param tag_metas: Collection that stores TagMeta instances with unique names. ''' self._obj_classes = ObjClassCollection() if obj_classes is None else obj_classes self._tag_metas = take_with_default(tag_metas, TagMetaCollection())
def _merge_img_obj_tag_metas(img_tag_metas: ObjClassCollection, obj_tag_metas: ObjClassCollection) -> ObjClassCollection: obj_tag_metas_to_add = [] for obj_tag_meta in obj_tag_metas: img_tag_meta_same_key = img_tag_metas.get(obj_tag_meta.key(), None) if img_tag_meta_same_key is None: obj_tag_metas_to_add.append(obj_tag_meta) elif not img_tag_meta_same_key.is_compatible(obj_tag_meta): raise ValueError( 'Unable to merge tag metas for images and objects. Found tags with the same name, but incompatible ' 'values. \n Image-level tag meta: {}\n Object-level tag meta: {}.\n Rename one of the tags to have a ' 'unique name to be able to load project meta.'.format(str(img_tag_meta_same_key), str(obj_tag_meta))) return img_tag_metas.add_items(obj_tag_metas_to_add)
def from_json(cls, data): ''' The function from_json convert ProjectMeta from json format to ProjectMeta class object. Generate exception error if all project tags not in a single collection :param data: input ProjectMeta in json format :return: ProjectMeta class object ''' tag_metas_json = data.get(ProjectMetaJsonFields.TAGS, []) img_tag_metas_json = data.get(ProjectMetaJsonFields.IMG_TAGS, []) obj_tag_metas_json = data.get(ProjectMetaJsonFields.OBJ_TAGS, []) project_type = data.get(ProjectMetaJsonFields.PROJECT_TYPE, None) if len(tag_metas_json) > 0: # New format - all project tags in a single collection. if any(len(x) > 0 for x in [img_tag_metas_json, obj_tag_metas_json]): raise ValueError( 'Project meta JSON contains both the {!r} section (current format merged tags for all of ' 'the project) and {!r} or {!r} sections (legacy format with separate collections for images ' 'and labeled objects). Either new format only or legacy format only are supported, but not a ' 'mix.'.format( ProjectMetaJsonFields.TAGS, ProjectMetaJsonFields.IMG_TAGS, ProjectMetaJsonFields.OBJ_TAGS)) tag_metas = TagMetaCollection.from_json(tag_metas_json) else: img_tag_metas = TagMetaCollection.from_json(img_tag_metas_json) obj_tag_metas = TagMetaCollection.from_json(obj_tag_metas_json) tag_metas = _merge_img_obj_tag_metas(img_tag_metas, obj_tag_metas) return cls(obj_classes=ObjClassCollection.from_json(data[ProjectMetaJsonFields.OBJ_CLASSES]), tag_metas=tag_metas, project_type=project_type)
def __init__(self, config: dict, in_meta: ProjectMeta, model: SingleImageInferenceBase): super().__init__(config, in_meta, model) # If saving the bounding boxes on which inference was called is requested, create separate classes # for those bounding boxes by renaming the source object classes. self._renamer_intermediate = None if self._config[SAVE]: renamer_intermediate = Renamer( add_suffix=self._config[Renamer.ADD_SUFFIX], enabled_classes=self._config[FROM_CLASSES]) # First simply rename the matching source classes. intermediate_renamed_classes = make_renamed_classes( in_meta.obj_classes, renamer_intermediate, skip_missing=True) # Next, change the geometry type for the intermediate bounding box classes to Rectangle. intermediate_renamed_rectangle_classes = ObjClassCollection(items=[ renamed_class.clone(geometry_type=Rectangle) for renamed_class in intermediate_renamed_classes ]) # Add the renamed Rectangle classes to the output meta and set up a class mapper. self._out_meta = self._out_meta.add_obj_classes( intermediate_renamed_rectangle_classes) self._intermediate_class_mapper = RenamingObjClassMapper( dest_obj_classes=intermediate_renamed_rectangle_classes, renamer=renamer_intermediate)
def to_json(self): ''' The function to_json convert annotation to json format :return: annotation in json format ''' res = { AnnotationJsonFields.IMG_DESCRIPTION: self.img_description, AnnotationJsonFields.IMG_SIZE: { AnnotationJsonFields.IMG_SIZE_HEIGHT: int(self.img_size[0]), AnnotationJsonFields.IMG_SIZE_WIDTH: int(self.img_size[1]) }, AnnotationJsonFields.IMG_TAGS: self.img_tags.to_json(), AnnotationJsonFields.LABELS: [label.to_json() for label in self.labels], AnnotationJsonFields.CUSTOM_DATA: self.custom_data } if len(self._pixelwise_scores_labels) > 0: # construct probability classes from labels prob_classes = {} for label in self._pixelwise_scores_labels: # @TODO: hotfix to save geometry as "multichannelBitmap" instead of "bitmap"; use normal classes prob_classes[label.obj_class.name] = label.obj_class.clone( geometry_type=MultichannelBitmap) # save probabilities probabilities = { AnnotationJsonFields.PROBABILITY_LABELS: [label.to_json() for label in self._pixelwise_scores_labels], AnnotationJsonFields.PROBABILITY_CLASSES: ObjClassCollection(list(prob_classes.values())).to_json() } res[AnnotationJsonFields.CUSTOM_DATA].update(probabilities) return res
def delete_obj_classes(self, obj_class_names): ''' The function delete_obj_classes delete objclasses with given list of names from ProjectMeta collection that stores ObjClass instances and return copy of ProjectMeta :param obj_class_names: list of names ObjClass objects to delete :return: ProjectMeta class object ''' res_items = self._delete_items(self._obj_classes, obj_class_names) return self.clone(obj_classes=ObjClassCollection(res_items))
def __init__(self, obj_classes=None, img_tag_metas=None, obj_tag_metas=None): self._obj_classes = take_with_default(obj_classes, ObjClassCollection()) # TODO do we actualy need two sets of tags? self._img_tag_metas = take_with_default(img_tag_metas, TagMetaCollection()) self._obj_tag_metas = take_with_default(obj_tag_metas, TagMetaCollection())
def _load_train_config(self): self._load_raw_model_config_json() self.class_title_to_idx = self.train_config[self.class_title_to_idx_key] logger.info('Read model internal class mapping', extra={'class_mapping': self.class_title_to_idx}) train_classes = ObjClassCollection.from_json(self.train_config[self.train_classes_key]) logger.info('Read model out classes', extra={'classes': train_classes.to_json()}) # TODO: Factor out meta constructing from _load_train_config method. self._model_out_meta = ProjectMeta(obj_classes=train_classes, tag_metas=self._model_out_tags()) # Make a separate [index] --> [class] map that excludes the 'special' classes that should not be in the` # final output. self.out_class_mapping = {idx: train_classes.get(title) for title, idx in self.class_title_to_idx.items() if train_classes.has_key(title)}
def _load_train_config(self): # @TODO: partly copypasted from SingleImageInferenceBase self._load_raw_model_config_json() self.classification_tags = self._model_out_img_tags() logger.info('Read model out tags', extra={'tags': self.classification_tags.to_json()}) self.classification_tags_to_idx = self.train_config[self.classification_tags_to_idx_key] logger.info('Read model internal tags mapping', extra={'tags_mapping': self.classification_tags_to_idx}) self._model_out_meta = ProjectMeta(obj_classes=ObjClassCollection(), img_tag_metas=self.classification_tags, obj_tag_metas=self._model_out_obj_tags()) self.idx_to_classification_tags = {v: k for k, v in self.classification_tags_to_idx.items()} self._determine_model_input_size()
def from_json(cls, data, project_meta): ''' The function from_json convert annotation from json format to Annotation class object. If one of the labels of annotation in json format cannot be convert to Label class object it generate exception error. :param data: input annotation in json format :param project_meta: ProjectMeta class object :return: Annotation class object ''' img_size_dict = data[AnnotationJsonFields.IMG_SIZE] img_height = img_size_dict[AnnotationJsonFields.IMG_SIZE_HEIGHT] img_width = img_size_dict[AnnotationJsonFields.IMG_SIZE_WIDTH] img_size = (img_height, img_width) try: labels = [ Label.from_json(label_json, project_meta) for label_json in data[AnnotationJsonFields.LABELS] ] except Exception: logger.fatal( 'Failed to deserialize annotation from JSON format. One of the Label objects could not be ' 'deserialized') raise custom_data = data.get(AnnotationJsonFields.CUSTOM_DATA, {}) prob_labels = None if AnnotationJsonFields.PROBABILITY_LABELS in custom_data and \ AnnotationJsonFields.PROBABILITY_CLASSES in custom_data: prob_classes = ObjClassCollection.from_json( custom_data[AnnotationJsonFields.PROBABILITY_CLASSES]) # @TODO: tony, maybe link with project meta (add probability classes???) prob_project_meta = ProjectMeta(obj_classes=prob_classes) prob_labels = [ Label.from_json(label_json, prob_project_meta) for label_json in custom_data[AnnotationJsonFields.PROBABILITY_LABELS] ] custom_data.pop(AnnotationJsonFields.PROBABILITY_CLASSES) custom_data.pop(AnnotationJsonFields.PROBABILITY_LABELS) return cls(img_size=img_size, labels=labels, img_tags=TagCollection.from_json( data[AnnotationJsonFields.IMG_TAGS], project_meta.tag_metas), img_description=data.get( AnnotationJsonFields.IMG_DESCRIPTION, ""), pixelwise_scores_labels=prob_labels, custom_data=custom_data)
def to_detection_task(self, convert_classes=False) -> (ProjectMeta, dict): mapping = {} res_classes = [] for obj_class in self.obj_classes: obj_class: ObjClass if obj_class.geometry_type == Rectangle: mapping[obj_class] = obj_class res_classes.append(obj_class) else: if convert_classes is True: new_class = obj_class.clone(geometry_type=Rectangle) mapping[obj_class] = new_class res_classes.append(new_class) else: # ignore class mapping[obj_class] = None res_meta = self.clone(obj_classes=ObjClassCollection(res_classes)) return res_meta, mapping
def setUp(self): self._obj_class_gt = ObjClass(name='a', geometry_type=Rectangle) self._obj_class_pred = ObjClass(name='b', geometry_type=Rectangle) self._confidence_tag_meta = TagMeta(name='confidence', value_type=TagValueType.ANY_NUMBER) self._meta = ProjectMeta( obj_classes=ObjClassCollection([self._obj_class_gt, self._obj_class_pred]), tag_metas=TagMetaCollection([self._confidence_tag_meta])) # Will match self._pred_obj_1 self._gt_obj_1 = Label(obj_class=self._obj_class_gt, geometry=Rectangle(0, 0, 10, 10)) # Will match self._pred_obj_3 self._gt_obj_2 = Label(obj_class=self._obj_class_gt, geometry=Rectangle(13, 13, 15, 15)) # Will be a false negative self._gt_obj_3 = Label(obj_class=self._obj_class_gt, geometry=Rectangle(43, 43, 45, 45)) # Will match self._gt_obj_1 self._pred_obj_1 = Label( obj_class=self._obj_class_pred, geometry=Rectangle(0, 0, 9, 9), tags=TagCollection([Tag(meta=self._confidence_tag_meta, value=0.7)])) # Will be a false positive (self._pred_obj_1 has higher IoU). self._pred_obj_2 = Label( obj_class=self._obj_class_pred, geometry=Rectangle(0, 0, 8, 8), tags=TagCollection([Tag(meta=self._confidence_tag_meta, value=0.6)])) # Will match self._gt_obj_2 self._pred_obj_3 = Label( obj_class=self._obj_class_pred, geometry=Rectangle(13, 13, 15, 15), tags=TagCollection([Tag(meta=self._confidence_tag_meta, value=0.1)])) # More false positives. self._pred_objs_fp = [ Label(obj_class=self._obj_class_pred, geometry=Rectangle(20, 20, 30, 30), tags=TagCollection([Tag(meta=self._confidence_tag_meta, value=v / 100)])) for v in range(15, 85, 10)] self._metric_calculator = MAPMetric(class_mapping={'a': 'b'}, iou_threshold=0.5)
def to_segmentation_task(self, keep_geometries=[Polygon, Bitmap]) -> (ProjectMeta, dict): mapping = {} res_classes = [] for obj_class in self.obj_classes: obj_class: ObjClass if obj_class.geometry_type in keep_geometries: if obj_class.geometry_type == Bitmap: mapping[obj_class] = obj_class res_classes.append(obj_class) else: new_class = obj_class.clone(geometry_type=Bitmap) mapping[obj_class] = new_class res_classes.append(new_class) else: mapping[obj_class] = None res_meta = self.clone(obj_classes=ObjClassCollection(res_classes)) return res_meta, mapping
def from_json(cls, data): return cls( ObjClassCollection.from_json( data[ProjectMetaJsonFields.OBJ_CLASSES]), TagMetaCollection.from_json(data[ProjectMetaJsonFields.IMG_TAGS]), TagMetaCollection.from_json(data[ProjectMetaJsonFields.OBJ_TAGS]))
def delete_obj_classes(self, obj_class_names): res_items = self._delete_items(self._obj_classes, obj_class_names) return self.clone(obj_classes=ObjClassCollection(res_items))
class ProjectMeta(JsonSerializable): ''' This is a class for creating and using ProjectMeta objects. This class contain data about meta information of the project ''' def __init__(self, obj_classes=None, tag_metas=None, project_type=None): ''' :param obj_classes: Collection that stores ObjClass instances with unique names. :param tag_metas: Collection that stores TagMeta instances with unique names. ''' self._obj_classes = ObjClassCollection() if obj_classes is None else obj_classes self._tag_metas = take_with_default(tag_metas, TagMetaCollection()) self._project_type = project_type @property def obj_classes(self) -> ObjClassCollection: return self._obj_classes @property def tag_metas(self) -> TagMetaCollection: return self._tag_metas @property def project_type(self): return self._project_type def to_json(self): ''' The function to_json convert ProjectMeta class object to json format :return: ProjectMeta in json format(dict) ''' res = { ProjectMetaJsonFields.OBJ_CLASSES: self._obj_classes.to_json(), ProjectMetaJsonFields.TAGS: self._tag_metas.to_json() } if self._project_type is not None: res[ProjectMetaJsonFields.PROJECT_TYPE] = self._project_type return res @classmethod def from_json(cls, data): ''' The function from_json convert ProjectMeta from json format to ProjectMeta class object. Generate exception error if all project tags not in a single collection :param data: input ProjectMeta in json format :return: ProjectMeta class object ''' tag_metas_json = data.get(ProjectMetaJsonFields.TAGS, []) img_tag_metas_json = data.get(ProjectMetaJsonFields.IMG_TAGS, []) obj_tag_metas_json = data.get(ProjectMetaJsonFields.OBJ_TAGS, []) project_type = data.get(ProjectMetaJsonFields.PROJECT_TYPE, None) if len(tag_metas_json) > 0: # New format - all project tags in a single collection. if any(len(x) > 0 for x in [img_tag_metas_json, obj_tag_metas_json]): raise ValueError( 'Project meta JSON contains both the {!r} section (current format merged tags for all of ' 'the project) and {!r} or {!r} sections (legacy format with separate collections for images ' 'and labeled objects). Either new format only or legacy format only are supported, but not a ' 'mix.'.format( ProjectMetaJsonFields.TAGS, ProjectMetaJsonFields.IMG_TAGS, ProjectMetaJsonFields.OBJ_TAGS)) tag_metas = TagMetaCollection.from_json(tag_metas_json) else: img_tag_metas = TagMetaCollection.from_json(img_tag_metas_json) obj_tag_metas = TagMetaCollection.from_json(obj_tag_metas_json) tag_metas = _merge_img_obj_tag_metas(img_tag_metas, obj_tag_metas) return cls(obj_classes=ObjClassCollection.from_json(data[ProjectMetaJsonFields.OBJ_CLASSES]), tag_metas=tag_metas, project_type=project_type) def merge(self, other): ''' Merge all instances from given meta to ProjectMeta and return it copy :param other: ProjectMeta class object :return: ProjectMeta class object ''' return self.clone(obj_classes=self._obj_classes.merge(other.obj_classes), tag_metas=self._tag_metas.merge(other._tag_metas)) def clone(self, obj_classes: ObjClassCollection = None, tag_metas: TagMetaCollection = None, project_type=None): ''' The function clone create copy of ProjectMeta with given Collections that stores ObjClass and TagMeta :param obj_classes: ObjClassCollection class object :param tag_metas: TagMetaCollection class object :return: ProjectMeta class object ''' return ProjectMeta(obj_classes=take_with_default(obj_classes, self.obj_classes), tag_metas=take_with_default(tag_metas, self.tag_metas), project_type=take_with_default(project_type, self.project_type)) def add_obj_class(self, new_obj_class): ''' The function add_obj_class add given objclass to ProjectMeta collection that stores ObjClass instances and return copy of ProjectMeta :param new_obj_class: ObjClass class object :return: ProjectMeta class object ''' return self.add_obj_classes([new_obj_class]) def add_obj_classes(self, new_obj_classes): ''' The function add_obj_class add given objclasses to ProjectMeta collection that stores ObjClass instances and return copy of ProjectMeta :param new_obj_classes: list of ObjClass class objects :return: ProjectMeta class object ''' return self.clone(obj_classes=self.obj_classes.add_items(new_obj_classes)) def add_tag_meta(self, new_tag_meta): ''' The function add_tag_meta add given tag to ProjectMeta collection that stores TagMeta instances and return copy of ProjectMeta :param new_tag_meta: TagMeta class object :return: ProjectMeta class object ''' return self.add_tag_metas([new_tag_meta]) def add_tag_metas(self, new_tag_metas): ''' The function add_tag_metas add given tags to ProjectMeta collection that stores TagMeta instances and return copy of ProjectMeta :param new_tag_metas: list of TagMeta class objects :return: ProjectMeta class object ''' return self.clone(tag_metas=self.tag_metas.add_items(new_tag_metas)) @staticmethod def _delete_items(collection, item_names): ''' :param collection: ObjClassCollection or TagMetaCollection instance :param item_names: list of item names to delete :return: list of items, which are in collection and not in given list of items to delete ''' names_to_delete = set(item_names) res_items = [] for item in collection: if item.key() not in names_to_delete: res_items.append(item) return res_items def delete_obj_class(self, obj_class_name): ''' The function delete_obj_class delete objclass with given name from ProjectMeta collection that stores ObjClass instances and return copy of ProjectMeta :param obj_class_name: str(name of ObjClass to detele from collection) :return: ProjectMeta class object ''' return self.delete_obj_classes([obj_class_name]) def delete_obj_classes(self, obj_class_names): ''' The function delete_obj_classes delete objclasses with given list of names from ProjectMeta collection that stores ObjClass instances and return copy of ProjectMeta :param obj_class_names: list of names ObjClass objects to delete :return: ProjectMeta class object ''' res_items = self._delete_items(self._obj_classes, obj_class_names) return self.clone(obj_classes=ObjClassCollection(res_items)) def delete_tag_meta(self, tag_name): ''' The function delete_tag_meta delete tag with given name from ProjectMeta collection that stores TagMeta instances and return copy of ProjectMeta :param tag_name: str(name of TagMeta to detele from collection) :return: ProjectMeta class object ''' return self.delete_tag_metas([tag_name]) def delete_tag_metas(self, tag_names): ''' The function delete_tag_metas delete tags with given list of names from ProjectMeta collection that stores TagMeta instances and return copy of ProjectMeta :param tag_names: list of names TagMeta objects to delete :return: ProjectMeta class object ''' res_items = self._delete_items(self._tag_metas, tag_names) return self.clone(tag_metas=TagMetaCollection(res_items)) def get_obj_class(self, obj_class_name): ''' :param obj_class_name: str :return: ObjClass class object with given name from ProjectMeta collection that stores ObjClass instances ''' return self._obj_classes.get(obj_class_name) def get_tag_meta(self, tag_name): ''' :param tag_name: str :return: TagMeta class object with given name from ProjectMeta collection that stores TagMeta instances ''' return self._tag_metas.get(tag_name) @staticmethod def merge_list(metas): ''' The function merge_list merge metas from given list of metas in single ProjectMeta class object :param metas: list of ProjectMeta objects :return: ProjectMeta class object ''' res_meta = ProjectMeta() for meta in metas: res_meta = res_meta.merge(meta) return res_meta def __str__(self): result = 'ProjectMeta:\n' result += 'Object Classes\n{}\n'.format(str(self._obj_classes)) result += 'Tags\n{}\n'.format(str(self._tag_metas)) return result