class LeafletMap: def __init__(self, bounds: tuple): self.layer = None self._leaflet_map = Map(layers=(basemap_to_tiles( basemaps.OpenStreetMap.BlackAndWhite), ), name="Leaflet Map", center=center(bounds), zoom=12, scroll_wheel_zoom=True) self._leaflet_map.add_control(FullScreenControl()) @property def map(self): return self._leaflet_map def update(self, layer: Layer): self.layer = layer self._remove_layers() self._update_layers() def _remove_layers(self): for layer in self._leaflet_map.layers: if isinstance(layer, TileLayer): continue self._leaflet_map.remove_layer(layer) def _update_layers(self): if self.layer.empty: return self._leaflet_map.add_layer(self.layer.layer)
class MapRegion: def __init__(self, center=(41.8204600, 1.8676800), zoom=9): self.map = Map(center=center, zoom=zoom, basemap=basemaps.OpenStreetMap.HOT) polygon = Polygon(locations=[[]], color="green", fill_color="green") def handle_click(**kwargs): if kwargs.get('type') == 'click': pol = next(layer for layer in self.map.layers if isinstance(layer, Polygon)) coords = kwargs.get('coordinates') if (len(polygon.locations) == 0): pol.locations[0].extend([coords, coords]) else: pol.locations[0].insert(1, coords) self.map.remove_layer(pol) other = Polygon(locations=pol.locations, color="green", fill_color="green") self.map.add_layer(other) if kwargs.get('type') == 'contextmenu': pol = next(layer for layer in self.map.layers if isinstance(layer, Polygon)) self.map.remove_layer(pol) other = Polygon(locations=[[]], color="green", fill_color="green") self.map.add_layer(other) self.map.on_interaction(handle_click) self.map.add_layer(polygon) display(self.map) def get_region(self): locations = [[]] for layer in self.map.layers: if isinstance(layer, Polygon): locations[0] = [[loc[1], loc[0]] for loc in layer.locations[0]] if (len(locations[0]) > 0): locations[0].append(locations[0][0]) return locations[0]
def clearLayers(worldMap: lf.Map, layerType: type, dispose: bool = True) -> int: ''' Remove all layers based on the given layerType @worldMap: Map object @layerType: type of layer to be removed @dispose: if True, delete the removed layer Returns: number of removed layers ''' layers = list( filter(lambda layer: isinstance(layer, layerType), worldMap.layers)) map(lambda layer: worldMap.remove_layer(layer), layers) if (dispose): for layer in layers: del layer return len(list(layers))
def removeLayerByType(worldMap: lf.Map, layerName: str, layerType: type, dispose: bool = True) -> bool: ''' Remove a heatmap layer by its name @worldMap: Map object @layerName: name of the layer @layerType: type of the layer @dispose: if True, delete the removed layer Returns: True if removed, False if not ''' layers = list( filter( lambda layer: isinstance(layer, layerType) and layer.name == layerName, worldMap.layers)) map(lambda layer: worldMap.remove_layer(layer), layers) if (dispose): for layer in layers: del layer return len(layers) > 0
class DatasetAnnotatorDetection: def __init__(self, classes, images, output_path, output_name, images_classes=None): # task classes self.__classes = classes self.__current_image_annotations = [] self.__current_class_for_ann = [] self.__output_path = output_path self.__output_name = output_name self.__file_path = os.path.join(self.__output_path, self.__output_name + ".json") self.__images = images # classes for each img self.__images_classes = images_classes self.__current_img = -1 self.__image_id = -1 self.__annotation_id = -1 self.__selected_ann = None if os.path.exists(self.__file_path): self.__load_coco_file() else: self.__create_coco_format() self.__id_for_class = {cl['name']: cl['id'] for cl in self.__CATEGORIES} self.__create_btns() self.__progress = widgets.IntProgress(min=0, max=len(images), value=1, description='{}/{}'.format(1, len(images))) self.__progress.bar_style = 'info' self.__title_lbl = widgets.Label(value="title") self.__create_map() self.__create_classes_btns() self.__validation_show = widgets.HTML(value="") self.__map_classes = widgets.HBox(children=[self.__map, widgets.VBox(children=self.__classes_btns)]) self.__all_widgets = widgets.VBox(children=[self.__progress, self.__buttons, self.__validation_show, self.__title_lbl, self.__map_classes]) # create coco file format def __create_coco_format(self): self.__INFO = { "description": "Dataset", "url": "www.polimi.it", "version": "0.1.0", "year": 2020, "contributor": "Polimi", "date_created": datetime.today().strftime('%Y-%m-%d-%H:%M:%S') } self.__LICENSES = [ { "id": 0, "name": "Attribution-NonCommercial-ShareAlike License", "url": "http://creativecommons.org/licenses/by-nc-sa/2.0/" } ] self.__create_categories() self.__IMAGES = [] self.__ANNOTATIONS = [] # load already creted coco file def __load_coco_file(self): with open(self.__file_path, 'r') as f: dictionary_with_data = json.load(f) self.__INFO = dictionary_with_data['info'] self.__LICENSES = dictionary_with_data['licenses'] self.__CATEGORIES = dictionary_with_data['categories'] self.__IMAGES = dictionary_with_data['images'] self.__ANNOTATIONS = dictionary_with_data['annotations'] assert set(self.__classes) == set( [c['name'] for c in self.__CATEGORIES]), "Classes in annotator and json must be equal" # get not used ids for annotations (deleted ann creates usable id) def __get_missing_ids(self, lst): ids = [el['id'] for el in lst] if len(ids) > 0: return sorted(set(range(min(ids), max(ids)+1)) - set(ids)) return [] # save current data to coco format def __save_coco_file(self): file_name = self.__get_image_name(self.__images[self.__current_img]) # remove annotations and image from list (will be added again later) self.__ANNOTATIONS = list(filter(lambda x: x['image_id'] != self.__image_id, self.__ANNOTATIONS)) self.__IMAGES = list(filter(lambda x: x['id'] != self.__image_id, self.__IMAGES)) h, w, hh, ww, off_h, off_w = self.__img_coords[:] free_ann_ids = self.__get_missing_ids(self.__ANNOTATIONS) #print('Missing ids', free_ann_ids) for idx, ann in enumerate(self.__current_image_annotations): ann_cl = self.__current_class_for_ann[idx] coordinates = ann.bounds # rectangle opposite coordinates wrt geojson hs = [c[0] for c in coordinates] ws = [c[1] for c in coordinates] min_h, max_h = max(hs), min(hs) min_w, max_w = min(ws), max(ws) h_ratio = h / hh w_ratio = w / ww # map coords to img coords min_h = (min_h - off_h) * h_ratio max_h = (max_h - off_h) * h_ratio min_w = (min_w - off_w) * w_ratio max_w = (max_w - off_w) * w_ratio min_h = h - min_h max_h = h - max_h if len(free_ann_ids) > 0: ann_id = free_ann_ids.pop(0) else: ann_id = len(self.__ANNOTATIONS) annotation_info = { "id": ann_id, "image_id": self.__image_id, "category_id": ann_cl, "iscrowd": False, # "area": area.tolist(), "bbox": [min_w, min_h, max_w - min_w, max_h - min_h], # "segmentation": segmentation } self.__annotation_id = ann_id self.__ANNOTATIONS.append(annotation_info) #print('Saved id', ann_id) image_info = { "id": self.__image_id, "file_name": file_name, "width": w, "height": h, "date_captured": datetime.today().strftime('%Y-%m-%d-%H:%M:%S'), "license": 0, # "coco_url": coco_url, # "flickr_url": flickr_url } self.__IMAGES.append(image_info) dictionary_with_data = {'info': self.__INFO, 'licenses': self.__LICENSES, 'categories': self.__CATEGORIES, 'images': self.__IMAGES, 'annotations': self.__ANNOTATIONS} with open(self.__file_path, 'w') as f: json.dump(dictionary_with_data, f, indent=4) # get image name def __get_image_name(self, img_path): return os.path.basename(img_path) def __get_image_name_no_ext(self, img_path): return os.path.splitext(self.__get_image_name(img_path))[0] # get image id def __get_current_image_id(self): img_name = self.__get_image_name(self.__images[self.__current_img]) for img in self.__IMAGES: if img['file_name'] == img_name: return img['id'] if len(self.__IMAGES) > 0: return max(self.__IMAGES, key=lambda x: x['id'])['id'] + 1 return 0 # load existing annotations def __load_annotations(self): img_id = self.__get_current_image_id() # resize coords to fit in map h_i, w_i, hh, ww, offset_h, offset_w = self.__img_coords[:] h_ratio = hh / h_i w_ratio = ww / w_i for ann in self.__ANNOTATIONS: if ann['image_id'] == img_id: # create rectangle layer min_w, min_h, w, h = ann['bbox'][:] max_w = min_w + w max_h = min_h + h min_h = h_i - min_h max_h = h_i - max_h # coords to map coords min_h = min_h * h_ratio + offset_h max_h = max_h * h_ratio + offset_h min_w = min_w * w_ratio + offset_w max_w = max_w * w_ratio + offset_w rectangle = self.__create_rectangle(((min_h, min_w), (max_h, max_w)), default_class=ann['category_id']) rectangle.color = "green" rectangle.fill_color = "green" self.__map.add_layer(rectangle) self.__current_image_annotations.append(rectangle) self.__current_class_for_ann.append(ann['category_id']) #print('Current annotations', self.__current_class_for_ann) # create coco categories def __create_categories(self): self.__CATEGORIES = [] for idx, cl in enumerate(self.__classes): proto = { 'id': idx, 'name': cl, 'supercategory': '' } self.__CATEGORIES.append(proto) # create buttons for navigation def __create_btns(self): self.__next_button = widgets.Button(description=labels_str.str_btn_next) self.__reset_button = widgets.Button(description=labels_str.str_btn_reset) self.__previous_button = widgets.Button(description=labels_str.str_btn_prev) self.__delete_button = widgets.Button(description=labels_str.str_btn_delete_bbox, disabled=True) self.__next_button.on_click(self.__on_next_button_clicked) self.__reset_button.on_click(self.__on_reset_button_clicked) self.__previous_button.on_click(self.__on_previous_button_clicked) self.__delete_button.on_click(self.__on_delete) self.__buttons = widgets.HBox(children=[self.__previous_button, self.__next_button, self.__delete_button, self.__reset_button]) # create radio buttons with classes def __create_classes_btns(self): # TODO: CHANGE TO RADIO BUTTON self.__classes_btns = [] for c in self.__classes: layout = widgets.Layout(width='200px',height='25px') box = widgets.Checkbox(value=False, description=c, disabled=True, layout=layout, indent=False, ) box.observe(self.__on_class_selected) self.__classes_btns.append(box) self.__classes_btns_interaction_disabled = False # reset all classes btns/checkbox/radio def __reset_classes_btns(self): # do not handle change value during reset self.__classes_btns_interaction_disabled = True for b in self.__classes_btns: b.value = False if self.__selected_class is None else b.description == self.__selected_class b.disabled = self.__selected_ann is None # enable handle change value for user interaction self.__classes_btns_interaction_disabled = False # interaction with classes btns/checkbox/radio def __on_class_selected(self, b): self.__clear_validation() if self.__classes_btns_interaction_disabled: return if self.__selected_ann is None: return if b['name'] == 'value': old_v = b['old'] new_v = b['new'] if new_v: self.__selected_class = b['owner'].description self.__current_class_for_ann[self.__selected_ann] = self.__classes.index(self.__selected_class) else: self.__selected_class = None self.__current_class_for_ann[self.__selected_ann] = None #print(old_v, new_v, self.__selected_class, self.__current_class_for_ann[self.__selected_ann]) self.__reset_classes_btns() # click on rectangle layer def __handle_click(self, **kwargs): if kwargs.get('type') == 'click': self.__clear_validation() click_coords = kwargs.get('coordinates') clicked_ann = None # find clicked annotations (can be more than 1 if overlapping) clicked_size = None for idx, ann in enumerate(self.__current_image_annotations): coordinates = ann.bounds # rectangle opposite coordinates wrt geojson hs = [c[0] for c in coordinates] ws = [c[1] for c in coordinates] min_h, max_h = min(hs), max(hs) min_w, max_w = min(ws), max(ws) # don't break so if two rectangles are overlapping I take only the last drawed if min_h <= click_coords[0] <= max_h and min_w <= click_coords[1] <= max_w: curr_size = (max_h - min_h) * (max_w - min_w) if clicked_size is None or curr_size < clicked_size: clicked_size = curr_size clicked_ann = idx if clicked_ann is not None: # change color to green # +2 because layer 0 is map, layer 1 is overlay self.__selected_ann = clicked_ann self.__delete_button.disabled = False self.__reset_colors_bboxes() current_class = self.__current_class_for_ann[self.__selected_ann] self.__selected_class = None if current_class is None else self.__classes[current_class] self.__reset_classes_btns() # it should not enter here because click is handled only by annotations else: self.__selected_ann = None self.__selected_class = None self.__reset_colors_bboxes() self.__reset_classes_btns() self.__delete_button.disabled = True # reset bboxes to green or red colors def __reset_colors_bboxes(self): for i in range(len(self.__current_image_annotations)): # blue selected annotation if self.__selected_ann is not None and i == self.__selected_ann: self.__map.layers[i + 2].color = "blue" self.__map.layers[i + 2].fill_color = "blue" # red annotation without class elif self.__current_class_for_ann[i] is None: self.__map.layers[i + 2].color = "red" self.__map.layers[i + 2].fill_color = "red" # green annotation with class else: self.__map.layers[i + 2].color = "green" self.__map.layers[i + 2].fill_color = "green" # delete selected layer, class and geojson def __on_delete(self, b): if not self.__selected_ann is None: # +2 because layer 0 is map, layer 1 is overlay and rectangles start from index 2 self.__map.remove_layer(self.__map.layers[self.__selected_ann + 2]) self.__current_image_annotations.pop(self.__selected_ann) self.__current_class_for_ann.pop(self.__selected_ann) # print('deleted') self.__selected_ann = None self.__selected_class = None self.__reset_colors_bboxes() self.__reset_classes_btns() self.__delete_button.disabled = True # create annotation rectangle def __create_rectangle(self, bounds, default_class=None): rectangle = Rectangle(bounds=bounds, color="red", fill_color="red") rectangle.on_click(self.__handle_click) mid_h = bounds[0][0] + (bounds[1][0] - bounds[0][0]) / 2 mid_w = bounds[0][1] + (bounds[1][1] - bounds[0][1]) / 2 #rectangle.popup = self.__create_popup((mid_h, mid_w), default_class) return rectangle # create and handle draw control def __create_draw_control(self): dc = DrawControl(rectangle={'shapeOptions': {'color': '#0000FF'}}, circle={}, circlemarker={}, polyline={}, marker={}, polygon={}) dc.edit = False dc.remove = False # handle drawing and deletion of annotations and corresponding classes def handle_draw(target, action, geo_json): # print(target) # print(action) # print(geo_json) if action == 'created' and geo_json['geometry']['type'] == 'Polygon': coordinates = geo_json['geometry']['coordinates'][0] #print(coordinates) #print(self.__map) #print(coordinates) hs = [c[1] for c in coordinates] ws = [c[0] for c in coordinates] min_h, max_h = min(hs), max(hs) min_w, max_w = min(ws), max(ws) # coordinates only inside image hh, ww, offset_h, offset_w = self.__img_coords[2:] max_h = max(0, min(hh + offset_h, max_h)) max_w = max(0, min(ww + offset_w, max_w)) min_h = max(offset_h, min(hh + offset_h, min_h)) min_w = max(offset_w, min(ww + offset_w, min_w)) # remove draw dc.clear() if max_h - min_h < 1 or max_w - min_w < 1: print(labels_str.warn_skip_wrong ) return # print(min_h, max_h, min_w, max_w) # create rectangle layer and remove drawn geojson rectangle = self.__create_rectangle(((min_h, min_w), (max_h, max_w))) self.__current_image_annotations.append(rectangle) self.__current_class_for_ann.append(None) self.__map.add_layer(rectangle) # automatically select last annotation self.__selected_ann = len(self.__current_image_annotations)-1 self.__reset_colors_bboxes() self.__selected_class = None self.__reset_classes_btns() self.__delete_button.disabled = False # print('Adding ann at index:',len(self.current_image_annotations)-1, # ' with class', self.current_class_for_ann[-1]) dc.on_draw(handle_draw) self.__map.add_control(dc) # load image and get map overlay def __get_img_overlay(self, img_path): # We need to create an ImageOverlay for each image to show, # and set the appropriate bounds based on the image size if not os.path.exists(img_path): print(labels_str.warn_img_path_not_exits + img_path) im = cv2.imread(img_path) h, w, _ = im.shape max_v = 100 offset_h = -25 offset_w = -25 hh = max_v - offset_h*2 ww = max_v - offset_w*2 if h > w: ww = int(w * hh / h) offset_w = (max_v - ww) / 2 elif w > h: hh = int(h * ww / w) offset_h = (max_v - hh) / 2 img_ov = ImageOverlay(url=img_path, bounds=((offset_h, offset_w), (hh + offset_h, ww+offset_w))) return img_ov, h, w, hh, ww, offset_h, offset_w # create and set map def __create_map(self): # Create the "map" that will be used to display the image # the crs simple, indicates that we will use pixels to locate objects inside the map self.__map = Map(center=(50, 50), zoom=2, crs=projections.Simple, dragging=False, zoom_control=False, double_click_zoom=False, layers=[LocalTileLayer(path='white.png')], layout=dict(width='600px', height='600px')) self.__create_draw_control() # remove all annotations from map def __clear_map(self, keep_img_overlay=False): starting_layer = 2 if keep_img_overlay else 1 for l in self.__map.layers[starting_layer:]: self.__map.remove_layer(l) self.__current_image_annotations = [] self.__current_class_for_ann = [] self.__selected_class = None self.__selected_ann = None self.__delete_button.disabled = True # disable or enable buttons def __toggle_interaction_buttons(self, disabled): self.__next_button.disabled = disabled self.__previous_button.disabled = disabled self.__reset_button.disabled = disabled # enable or disable buttons for first and last images def __change_buttons_status(self): self.__next_button.disabled = self.__current_img >= len(self.__images) - 1 self.__previous_button.disabled = self.__current_img <= 0 def __clear_workspace(self): self.__toggle_interaction_buttons(disabled=True) self.__save_coco_file() self.__clear_map() self.__reset_classes_btns() self.__clear_validation() def __clear_validation(self): self.__validation_show.value = "" # next button clicked def __on_next_button_clicked(self, b): if None in self.__current_class_for_ann: self.__validation_show.value = labels_str.warn_select_class return self.__clear_workspace() self.__show_image(1) # previous button clicked def __on_previous_button_clicked(self, b): if None in self.__current_class_for_ann: return self.__clear_workspace() self.__show_image(-1) # reset button clicked def __on_reset_button_clicked(self, b): self.__clear_map(keep_img_overlay=True) self.__reset_classes_btns() # show current image def __show_image(self, delta): # update progress bar self.__current_img += delta self.__progress.value = self.__current_img + 1 self.__progress.description = '{}/{}'.format(self.__current_img + 1, len(self.__images)) self.__toggle_interaction_buttons(disabled=False) # change buttons self.__change_buttons_status() # update map img_ov, h, w, hh, ww, off_h, off_w = self.__get_img_overlay(self.__images[self.__current_img]) self.__img_coords = (h, w, hh, ww, off_h, off_w) self.__map.add_layer(img_ov) self.__image_id = self.__get_current_image_id() # update title file_name = self.__get_image_name_no_ext(self.__images[self.__current_img]) self.__title_lbl.value = file_name if self.__images_classes is not None: pass # do something with additional infos # load current annotations self.__load_annotations() def start_annotation(self): display(self.__all_widgets) self.__show_image(1)
class DomainPicker: def __init__(self, n_days_old_satimg=1): t = datetime.datetime.now() - datetime.timedelta(days=n_days_old_satimg) t_str = t.strftime("%Y-%m-%d") self.m = Map( layers=[ basemap_to_tiles(basemaps.NASAGIBS.ModisTerraTrueColorCR, t_str), ], center=(52.204793, 360.121558), zoom=2, ) self.domain_coords = [] self.polygon = None self.marker_locs = {} self.m.on_interaction(self._handle_map_click) button_reset = Button(description="reset") button_reset.on_click(self._clear_domain) button_save = Button(description="save domain") button_save.on_click(self._save_domain) self.name_textfield = Text(value="domain_name", width=10) self.m.add_control(WidgetControl(widget=button_save, position="bottomright")) self.m.add_control(WidgetControl(widget=button_reset, position="bottomright")) self.m.add_control( WidgetControl(widget=self.name_textfield, position="bottomright") ) def _update_domain_render(self): if self.polygon is not None: self.m.remove_layer(self.polygon) if len(self.domain_coords) > 1: self.polygon = Polygon( locations=self.domain_coords, color="green", fill_color="green" ) self.m.add_layer(self.polygon) else: self.polygon = None def _handle_marker_move(self, marker, location, **kwargs): old_loc = marker.location new_loc = location idx = self.domain_coords.index(old_loc) self.domain_coords[idx] = new_loc self._update_domain_render() def _handle_map_click(self, **kwargs): if kwargs.get("type") == "click": loc = kwargs.get("coordinates") marker = Marker(location=loc) marker.on_move(partial(self._handle_marker_move, marker=marker)) self.domain_coords.append(loc) self.marker_locs[marker] = loc self.m.add_layer(marker) self._update_domain_render() def _clear_domain(self, *args, **kwargs): self.domain_coords = [] for marker in self.marker_locs.keys(): self.m.remove_layer(marker) self.marker_locs = {} self._update_domain_render() def _save_domain(self, *args, **kwargs): fn = "{}.domain.yaml".format(self.name_textfield.value) with open(fn, "w") as fh: yaml.dump(self.domain_coords, fh, default_flow_style=False) print("Domain points written to `{}`".format(fn))
class BBoxSelector: def __init__(self, bbox, zoom=8, resolution=10): center = (bbox.min_y + bbox.max_y) / 2, (bbox.min_x + bbox.max_x) / 2 self.map = Map(center=center, zoom=zoom, scroll_wheel_zoom=True) self.resolution = resolution control = DrawControl() control.rectangle = { "shapeOptions": { "fillColor": "#fabd14", "color": "#fa6814", "fillOpacity": 0.2 } } #Disable the rest of draw options control.polyline = {} control.circle = {} control.circlemarker = {} control.polygon = {} control.edit = False control.remove = False control.on_draw(self._handle_draw) self.map.add_control(control) self.bbox = None self.size = None self.rectangle = None self.add_rectangle(bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y) # self.out = w.Output(layout=w.Layout(width='100%', height='50px', overflow_y='scroll')) # self.vbox = w.VBox([self.map, self.out]) def add_rectangle(self, min_x, min_y, max_x, max_y): if self.rectangle: self.map.remove_layer(self.rectangle) self.rectangle = Rectangle( bounds=((min_y, min_x), (max_y, max_x)), color="#fa6814", fill=True, fill_color="#fabd14", fill_opacity=0.2, weight=1 ) self.map.add_layer(self.rectangle) self.bbox = BBox(((min_x, min_y), (max_x, max_y)), CRS.WGS84).transform(CRS.POP_WEB) # self.out.append_display_data((min_x, min_y, max_x, max_y)) size_x = abs(int((self.bbox.max_x - self.bbox.min_x) / self.resolution)) size_y = abs(int((self.bbox.max_y - self.bbox.min_y) / self.resolution)) self.size = size_x, size_y def _handle_draw(self, control, action, geo_json): control.clear_rectangles() bbox_geom = geo_json['geometry']['coordinates'][0] min_x, min_y = bbox_geom[0] max_x, max_y = bbox_geom[2] self.add_rectangle(min_x, min_y, max_x, max_y) def show(self): return self.map