def initialize_ui(self): # clear all information self.txb_statistics.setText('') self.lbl_sample_image.setText('') self.btn_previous_image.setEnabled(False) self.btn_next_image.setEnabled(False) # Create text with ground truth statistics if self.type_bb == BBType.GROUND_TRUTH: stats = self.text_statistics.replace('#TYPE_BB#', 'Ground Truth') self.annot_obj = self.gt_annotations elif self.type_bb == BBType.DETECTED: stats = self.text_statistics.replace('#TYPE_BB#', 'Detections') self.annot_obj = self.det_annotations self.chb_det_bb.setVisible(False) self.chb_gt_bb.setVisible(False) if self.det_annotations is not None and self.det_annotations != []: self.chb_det_bb.setVisible(True) if self.gt_annotations is not None and self.gt_annotations != []: self.chb_gt_bb.setVisible(True) stats = stats.replace('#TOTAL_BB#', str(len(self.annot_obj))) stats = stats.replace( '#TOTAL_IMAGES#', str(BoundingBox.get_total_images(self.annot_obj))) stats = stats.replace( '#AVERAGE_AREA_BB#', '%.2f' % BoundingBox.get_average_area(self.annot_obj)) # Get amount of bounding boxes per class self.bb_per_class = BoundingBox.get_amount_bounding_box_all_classes( self.annot_obj) amount_bb_per_class = 'No class found' if len(self.bb_per_class) > 0: amount_bb_per_class = '' longest_class_name = len(max(self.bb_per_class.keys(), key=len)) for c, amount in self.bb_per_class.items(): c = c.ljust(longest_class_name, ' ') amount_bb_per_class += f' {c} : {amount}<br>' stats = stats.replace('#AMOUNT_BB_PER_CLASS#', amount_bb_per_class) self.txb_statistics.setText(stats) # get first image file and show it if os.path.isdir(self.dir_images): self.image_files = get_files_dir( self.dir_images, extensions=['jpg', 'jpge', 'png', 'bmp', 'tiff', 'tif']) if len(self.image_files) > 0: self.selected_image_index = 0 else: self.selected_image_index = -1 else: self.image_files = [] self.selected_image_index = -1 self.show_image()
def cvat2bb(path): '''This format supports ground-truth only''' ret = [] # Get annotation files in the path annotation_files = _get_annotation_files(path) # Loop through each file for file_path in annotation_files: if not validations.is_cvat_format(file_path): continue # Loop through the images for image_info in ET.parse(file_path).iter('image'): img_size = (int(image_info.attrib['width']), int(image_info.attrib['height'])) img_name = image_info.attrib['name'] img_name = general_utils.get_file_name_only(img_name) # Loop through the boxes for box_info in image_info.iter('box'): x1, y1, x2, y2 = float(box_info.attrib['xtl']), float( box_info.attrib['ytl']), float( box_info.attrib['xbr']), float(box_info.attrib['ybr']) x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) bb = BoundingBox(image_name=img_name, class_id=box_info.attrib['label'], coordinates=(x1, y1, x2, y2), img_size=img_size, type_coordinates=CoordinatesType.ABSOLUTE, bb_type=BBType.GROUND_TRUTH, format=BBFormat.XYX2Y2) ret.append(bb) return ret
def imagenet2bb(annotations_path): ret = [] # Get annotation files in the path annotation_files = _get_annotation_files(annotations_path) # Loop through each file for file_path in annotation_files: if not validations.is_imagenet_format(file_path): continue # Open XML img_name = ET.parse(file_path).find('filename').text img_name = general_utils.get_file_name_only(img_name) img_width = int(ET.parse(file_path).find('size/width').text) img_height = int(ET.parse(file_path).find('size/height').text) img_size = (img_width, img_height) # Loop through the detections for box_info in ET.parse(file_path).iter('object'): obj_class = box_info.find('name').text x1 = int(float(box_info.find('bndbox/xmin').text)) y1 = int(float(box_info.find('bndbox/ymin').text)) x2 = int(float(box_info.find('bndbox/xmax').text)) y2 = int(float(box_info.find('bndbox/ymax').text)) bb = BoundingBox(image_name=img_name, class_id=obj_class, coordinates=(x1, y1, x2, y2), img_size=img_size, type_coordinates=CoordinatesType.ABSOLUTE, bb_type=BBType.GROUND_TRUTH, format=BBFormat.XYX2Y2) ret.append(bb) return ret
def __init__(self, image_path: str, waldo_bounding_box_json_file_path: str, debug: bool = False): logging_utils.init_logger(log_level_for_console='debug' if debug is True else 'info') self._logger = logging_utils.get_logger() self.waldo_bounding_box = BoundingBox.from_json(waldo_bounding_box_json_file_path) self._fail_circle_radius = int(min([self.waldo_bounding_box.right - self.waldo_bounding_box.left, self.waldo_bounding_box.bottom - self.waldo_bounding_box.top]) / 2) image = Image.open(image_path).transpose(Image.FLIP_TOP_BOTTOM) self._aspect_ratio = round(image.size[0] / image.size[1], 5) self.window_size: [int, int] = self._compute_initial_window_size(image.size) self.context: moderngl.Context = moderngl.create_standalone_context() self.graphic_engine = GraphicEngine(self.context, self.window_size, image) self.root = tk.Tk() self.root.title(self.WINDOW_TITLE) self.root.geometry(f'{self.window_size[0]}x{self.window_size[1]}') self.main_canvas: tk.Canvas = tk.Canvas(self.root) self.main_canvas.place(relwidth=1, relheight=1, anchor=tk.NW) self.framebuffer: TkinterFramebuffer = TkinterFramebuffer(self.context, self.window_size) self._add_framebuffer_image_to_canvas() self.root.protocol("WM_DELETE_WINDOW", self.before_closing) self.main_canvas.bind("<Configure>", self.on_resize) self.main_canvas.bind("<ButtonPress-1>", self.on_mouse_left_button_press) self.main_canvas.bind_all("<Key>", self.on_key_press) self._detections_circle_center_xy: [[float, float]] = []
def yolo2bb(annotations_path, images_dir, file_obj_names, bb_type=BBType.GROUND_TRUTH): ret = [] if not os.path.isfile(file_obj_names): print(f'Warning: File with names of classes {file_obj_names} not found.') return ret # Load classes all_classes = [] with open(file_obj_names, "r") as f: all_classes = [line.replace('\n', '') for line in f] # Get annotation files in the path annotation_files = _get_annotation_files(annotations_path) # Loop through each file for file_path in annotation_files: if not validations.is_yolo_format(file_path, bb_types=[bb_type]): continue img_name = os.path.basename(file_path) img_file = general_utils.find_file(images_dir, img_name, match_extension=False) img_resolution = general_utils.get_image_resolution(img_file) if img_resolution is None: print(f'Warning: It was not possible to find the resolution of image {img_name}') continue img_size = (img_resolution['width'], img_resolution['height']) # Loop through lines with open(file_path, "r") as f: for line in f: splitted_line = line.split(' ') class_id = splitted_line[0] if not general_utils.is_str_int(class_id): print( f'Warning: Class id represented in the {file_path} is not a valid integer.') return [] class_id = int(class_id) if class_id not in range(len(all_classes)): print( f'Warning: Class id represented in the {file_path} is not in the range of classes specified in the file {file_obj_names}.' ) return [] if bb_type == BBType.GROUND_TRUTH: confidence = None x1 = float(splitted_line[1]) y1 = float(splitted_line[2]) w = float(splitted_line[3]) h = float(splitted_line[4]) elif bb_type == BBType.DETECTED: confidence = float(splitted_line[1]) x1 = float(splitted_line[2]) y1 = float(splitted_line[3]) w = float(splitted_line[4]) h = float(splitted_line[5]) bb = BoundingBox(image_name=general_utils.get_file_name_only(img_file), class_id=all_classes[class_id], coordinates=(x1, y1, w, h), img_size=img_size, confidence=confidence, type_coordinates=CoordinatesType.RELATIVE, bb_type=bb_type, format=BBFormat.XYWH) ret.append(bb) return ret
def openimage2bb(annotations_path, images_dir, bb_type=BBType.GROUND_TRUTH): ret = [] # Get annotation files in the path annotation_files = _get_annotation_files(annotations_path) # Loop through each file for file_path in annotation_files: if not validations.is_openimage_format(file_path): continue images_shapes = {} # Open csv csv = pd.read_csv(file_path, sep=',') for i, row in csv.iterrows(): # Get image resolution if it was not loaded yet if row['ImageID'] not in images_shapes: img_name = row['ImageID'] image_file = general_utils.find_file(images_dir, img_name) images_shapes[image_file] = general_utils.get_image_resolution( image_file) if images_shapes[image_file] is None: print( f'Warning: It was not possible to find the resolution of image {img_name}' ) continue # Three is no bounding box for the given image if pd.isna(row['LabelName']) or pd.isna(row['XMin']) or pd.isna( row['XMax']) or pd.isna(row['YMin']) or pd.isna( row['YMax']): continue # images_shapes[image_file] = general_utils.get_image_resolution(image_file) img_size = (images_shapes[image_file]['width'], images_shapes[image_file]['height']) x1, x2, y1, y2 = (row['XMin'], row['XMax'], row['YMin'], row['YMax']) x1 = x1.replace(',', '.') if isinstance(x1, str) else x1 x2 = x2.replace(',', '.') if isinstance(x2, str) else x2 y1 = y1.replace(',', '.') if isinstance(y1, str) else y1 y2 = y2.replace(',', '.') if isinstance(y2, str) else y2 x1, x2, y1, y2 = float(x1), float(x2), float(y1), float(y2) confidence = None if pd.isna(row['Confidence']) else float( row['Confidence']) if bb_type == BBType.DETECTED and confidence is None: print( f'Warning: Confidence value found in the CSV file for the image {img_name}' ) return ret bb = BoundingBox(image_name=general_utils.get_file_name_only( row['ImageID']), class_id=row['LabelName'], coordinates=(x1, y1, x2, y2), img_size=img_size, confidence=confidence, type_coordinates=CoordinatesType.RELATIVE, bb_type=bb_type, format=BBFormat.XYX2Y2) ret.append(bb) return ret
def coco2bb(path, bb_type=BBType.GROUND_TRUTH): ret = [] # Get annotation files in the path annotation_files = _get_annotation_files(path) # Loop through each file for file_path in annotation_files: if not validations.is_coco_format(file_path): continue with open(file_path, "r") as f: json_object = json.load(f) # COCO json file contains basically 3 lists: # categories: containing the classes # images: containing information of the images (width, height and filename) # annotations: containing information of the bounding boxes (x1, y1, bb_width, bb_height) classes = {} if 'categories' in json_object: classes = json_object['categories'] # into dictionary classes = {c['id']: c['name'] for c in classes} images = {} # into dictionary for i in json_object['images']: images[i['id']] = { 'file_name': i['file_name'], 'img_size': (int(i['width']), int(i['height'])) } annotations = [] if 'annotations' in json_object: annotations = json_object['annotations'] for annotation in annotations: img_id = annotation['image_id'] x1, y1, bb_width, bb_height = annotation['bbox'] if bb_type == BBType.DETECTED and 'score' not in annotation.keys(): print('Warning: Confidence not found in the JSON file!') return ret confidence = annotation[ 'score'] if bb_type == BBType.DETECTED else None # Make image name only the filename, without extension img_name = images[img_id]['file_name'] img_name = general_utils.get_file_name_only(img_name) # create BoundingBox object bb = BoundingBox(image_name=img_name, class_id=classes[annotation['category_id']], coordinates=(x1, y1, bb_width, bb_height), type_coordinates=CoordinatesType.ABSOLUTE, img_size=images[img_id]['img_size'], confidence=confidence, bb_type=bb_type, format=BBFormat.XYWH) ret.append(bb) return ret
def to_bb(index: int, annotation: AnnotatedBBox) -> BoundingBox: image_id = str(index) return BoundingBox( image_name=image_id, class_id=annotation.class_name, coordinates=annotation.bbox.xywh(), format=BBFormat.XYWH, bb_type=BBType.GROUND_TRUTH if annotation.type == AnnotationType.GROUND_TRUTH else BBType.DETECTED, confidence=annotation.confidence if annotation.type == AnnotationType.PREDICTION else None, )
def draw_bounding_boxes(self): # Load image to obtain a clean image (without BBs) img_path = os.path.join(self.dir_images, self.image_files[self.selected_image_index]) img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Get bounding boxes of the loaded image img_name = self.image_files[self.selected_image_index] img_name = general_utils.get_file_name_only(img_name) # Add bounding boxes depending if the item is checked if self.chb_gt_bb.isChecked() and self.gt_annotations is not None: bboxes = BoundingBox.get_bounding_boxes_by_image_name( self.gt_annotations, img_name) if len(bboxes) == 0: bboxes = BoundingBox.get_bounding_boxes_by_image_name( self.gt_annotations, img_name) # Draw bounding boxes for bb in bboxes: img = add_bb_into_image(img, bb, color=(0, 255, 0), thickness=2, label=None) if self.chb_det_bb.isChecked() and self.det_annotations is not None: bboxes = BoundingBox.get_bounding_boxes_by_image_name( self.det_annotations, img_name) if len(bboxes) == 0: bboxes = BoundingBox.get_bounding_boxes_by_image_name( self.det_annotations, img_name) # Draw bounding boxes for bb in bboxes: img = add_bb_into_image(img, bb, color=(0, 0, 255), thickness=2, label=None) return img
def main(): logging.basicConfig(filename='preproces_dateset.log', level=logging.DEBUG) logging.info("Scanning content of dataset") content = file.scan_content(config.set_path) logging.info("Dividing data into groups") divided_content = divide_dataset.divide(content) bounding_boxes = BoundingBox.get_bounding_boxes(config.bounding_boxes_path) logging.info("Starting image preprocessing") counter = 0 for key in divided_content.keys(): augment = False if key == "training": augment = True logging.info("Training data will be augmented.") database = h5py.File(config.get_convolution_datasets_path(key), 'w') for (cls_name, img_name) in divided_content[key]: counter += 1 if counter % config.take_every_nth_sample != 0: continue if cls_name not in database.keys(): database.create_group(cls_name) cls_path = file.add_folder(config.set_path, cls_name) if augment: augmented_data = augment_images( load_and_preprocess_img(cls_path, img_name, bounding_boxes)) augmented_files = [ file.remove_extension(img_name) + "_" + str(i) for i in range(len(augmented_data)) ] for img, name in zip(augmented_data, augmented_files): database[cls_name].create_dataset(name, data=img) else: database[cls_name].create_dataset( file.remove_extension(img_name), data=load_and_preprocess_img(cls_path, img_name, bounding_boxes)) database.close() logging.info("Image loading finished")
def labelme2bb(annotations_path): ret = [] # Get annotation files in the path annotation_files = _get_annotation_files(annotations_path) # Loop through each file for file_path in annotation_files: if not validations.is_labelme_format(file_path): continue # Parse the JSON file with open(file_path, "r") as f: json_object = json.load(f) img_path = json_object['imagePath'] img_path = os.path.basename(img_path) img_path = general_utils.get_file_name_only(img_path) img_size = (int(json_object['imageWidth']), int(json_object['imageHeight'])) # If there are annotated objects if 'shapes' in json_object: # Loop through bounding boxes for obj in json_object['shapes']: obj_label = obj['label'] ((x1, y1), (x2, y2)) = obj['points'] # If there is no bounding box annotations, bb coordinates could have been set to None if x1 is None and y1 is None and x2 is None and y2 is None: continue x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int( float(x2)), int(float(y2)) bb = BoundingBox(image_name=img_path, class_id=obj_label, coordinates=(x1, y1, x2, y2), img_size=img_size, confidence=None, type_coordinates=CoordinatesType.ABSOLUTE, bb_type=BBType.GROUND_TRUTH, format=BBFormat.XYX2Y2) ret.append(bb) return ret
for photo in split_ids[group_name]['data']: class_name = photo[0] if class_name not in group_db.keys(): group_db.create_group(class_name) for i in range(0, config.data_multiplication_factor): photo_name = photo[1] + "_" + str(i) group_db[class_name].create_dataset( photo_name, data=features_db[class_name][photo_name]) group_db.close() if __name__ == "__main__": logging.basicConfig(filename="sift.log", level=logging.DEBUG) features_db = h5py.File(config.features_db_path, "w") bounding_boxes = BoundingBox.get_bounding_boxes(config.bounding_boxes_path) counter = 0 logging.info("Starting extraction") for class_path in file.gen_subdir_path(config.set_path): class_descriptors = features_db.create_group( file.get_folder(class_path)) for photo_path, photo_name in file.gen_file_path(class_path): counter += 1 if counter % config.take_every_nth_sample != 0: continue # removes file extension photo_name_hash = file.remove_extension(photo_name) bb = bounding_boxes[photo_name_hash] photo_desc = execute_sift_extraction(photo_path, bb, 1) for i, pic in enumerate(photo_desc):
def text2bb(annotations_path, bb_type=BBType.GROUND_TRUTH, bb_format=BBFormat.XYWH, type_coordinates=CoordinatesType.ABSOLUTE, img_dir=None): ret = [] # Get annotation files in the path annotation_files = _get_annotation_files(annotations_path) for file_path in annotation_files: if type_coordinates == CoordinatesType.ABSOLUTE: if bb_type == BBType.GROUND_TRUTH and not validations.is_absolute_text_format( file_path, num_blocks=[5], blocks_abs_values=[4]): continue if bb_type == BBType.DETECTED and not validations.is_absolute_text_format( file_path, num_blocks=[6], blocks_abs_values=[4]): continue elif type_coordinates == CoordinatesType.RELATIVE: if bb_type == BBType.GROUND_TRUTH and not validations.is_relative_text_format( file_path, num_blocks=[5], blocks_rel_values=[4]): continue if bb_type == BBType.DETECTED and not validations.is_relative_text_format( file_path, num_blocks=[6], blocks_rel_values=[4]): continue # Loop through lines with open(file_path, "r") as f: img_filename = os.path.basename(file_path) img_filename = os.path.splitext(img_filename)[0] img_size = None # If coordinates are relative, image size must be obtained in the img_dir if type_coordinates == CoordinatesType.RELATIVE: img_path = general_utils.find_file(img_dir, img_filename, match_extension=False) if img_path is None or os.path.isfile(img_path) is False: print( f'Warning: Image not found in the directory {img_path}. It is required to get its dimensions' ) return ret resolution = general_utils.get_image_resolution(img_path) img_size = (resolution['width'], resolution['height']) for line in f: if line.replace(' ', '') == '\n': continue splitted_line = line.split(' ') class_id = splitted_line[0] if bb_type == BBType.GROUND_TRUTH: confidence = None x1 = float(splitted_line[1]) y1 = float(splitted_line[2]) w = float(splitted_line[3]) h = float(splitted_line[4]) elif bb_type == BBType.DETECTED: confidence = float(splitted_line[1]) x1 = float(splitted_line[2]) y1 = float(splitted_line[3]) w = float(splitted_line[4]) h = float(splitted_line[5]) bb = BoundingBox(image_name=img_filename, class_id=class_id, coordinates=(x1, y1, w, h), img_size=img_size, confidence=confidence, type_coordinates=type_coordinates, bb_type=bb_type, format=bb_format) # If the format is correct, x,y,w,h,x2,y2 must be positive x, y, w, h = bb.get_absolute_bounding_box(format=BBFormat.XYWH) _, _, x2, y2 = bb.get_absolute_bounding_box( format=BBFormat.XYX2Y2) if x < 0 or y < 0 or w < 0 or h < 0 or x2 < 0 or y2 < 0: continue ret.append(bb) return ret
def get_pascalvoc_metrics(gt_boxes, det_boxes, iou_threshold=0.5, method=MethodAveragePrecision.EVERY_POINT_INTERPOLATION, generate_table=False): """Get the metrics used by the VOC Pascal 2012 challenge. Args: boundingboxes: Object of the class BoundingBoxes representing ground truth and detected bounding boxes; iou_threshold: IOU threshold indicating which detections will be considered TP or FP (dget_pascalvoc_metricsns: A dictioanry contains information and metrics of each class. The key represents the class and the values are: dict['class']: class representing the current dictionary; dict['precision']: array with the precision values; dict['recall']: array with the recall values; dict['AP']: average precision; dict['interpolated precision']: interpolated precision values; dict['interpolated recall']: interpolated recall values; dict['total positives']: total number of ground truth positives; dict['total TP']: total number of True Positive detections; dict['total FP']: total number of False Positive detections;""" ret = {} # Get classes of all bounding boxes separating them by classes gt_classes_only = [] classes_bbs = {} for bb in gt_boxes: c = bb.get_class_id() gt_classes_only.append(c) classes_bbs.setdefault(c, {'gt': [], 'det': []}) classes_bbs[c]['gt'].append(bb) gt_classes_only = list(set(gt_classes_only)) for bb in det_boxes: c = bb.get_class_id() classes_bbs.setdefault(c, {'gt': [], 'det': []}) classes_bbs[c]['det'].append(bb) # Precision x Recall is obtained individually by each class for c, v in classes_bbs.items(): # Report results only in the classes that are in the GT if c not in gt_classes_only: continue npos = len(v['gt']) # sort detections by decreasing confidence dects = [a for a in sorted(v['det'], key=lambda bb: bb.get_confidence(), reverse=True)] TP = np.zeros(len(dects)) FP = np.zeros(len(dects)) # create dictionary with amount of expected detections for each image detected_gt_per_image = Counter([bb.get_image_name() for bb in gt_boxes]) for key, val in detected_gt_per_image.items(): detected_gt_per_image[key] = np.zeros(val) # print(f'Evaluating class: {c}') dict_table = { 'image': [], 'confidence': [], 'TP': [], 'FP': [], 'acc TP': [], 'acc FP': [], 'precision': [], 'recall': [] } # Loop through detections for idx_det, det in enumerate(dects): img_det = det.get_image_name() if generate_table: dict_table['image'].append(img_det) dict_table['confidence'].append(f'{100*det.get_confidence():.2f}%') # Find ground truth image gt = [gt for gt in classes_bbs[c]['gt'] if gt.get_image_name() == img_det] # Get the maximum iou among all detectins in the image iouMax = sys.float_info.min # Given the detection det, find ground-truth with the highest iou for j, g in enumerate(gt): # print('Ground truth gt => %s' % # str(g.get_absolute_bounding_box(format=BBFormat.XYX2Y2))) iou = BoundingBox.iou(det, g) if iou > iouMax: iouMax = iou id_match_gt = j # Assign detection as TP or FP if iouMax >= iou_threshold: # gt was not matched with any detection if detected_gt_per_image[img_det][id_match_gt] == 0: TP[idx_det] = 1 # detection is set as true positive detected_gt_per_image[img_det][ id_match_gt] = 1 # set flag to identify gt as already 'matched' # print("TP") if generate_table: dict_table['TP'].append(1) dict_table['FP'].append(0) else: FP[idx_det] = 1 # detection is set as false positive if generate_table: dict_table['FP'].append(1) dict_table['TP'].append(0) # print("FP") # - A detected "cat" is overlaped with a GT "cat" with IOU >= iou_threshold. else: FP[idx_det] = 1 # detection is set as false positive if generate_table: dict_table['FP'].append(1) dict_table['TP'].append(0) # print("FP") # compute precision, recall and average precision acc_FP = np.cumsum(FP) acc_TP = np.cumsum(TP) rec = acc_TP / npos prec = np.divide(acc_TP, (acc_FP + acc_TP)) if generate_table: dict_table['acc TP'] = list(acc_TP) dict_table['acc FP'] = list(acc_FP) dict_table['precision'] = list(prec) dict_table['recall'] = list(rec) table = pd.DataFrame(dict_table) else: table = None # Depending on the method, call the right implementation if method == MethodAveragePrecision.EVERY_POINT_INTERPOLATION: [ap, mpre, mrec, ii] = calculate_ap_every_point(rec, prec) elif method == MethodAveragePrecision.ELEVEN_POINT_INTERPOLATION: [ap, mpre, mrec, _] = calculate_ap_11_point_interp(rec, prec) else: Exception('method not defined') # add class result in the dictionary to be returned ret[c] = { 'precision': prec, 'recall': rec, 'AP': ap, 'interpolated precision': mpre, 'interpolated recall': mrec, 'total positives': npos, 'total TP': np.sum(TP), 'total FP': np.sum(FP), 'method': method, 'iou': iou_threshold, 'table': table } # For mAP, only the classes in the gt set should be considered mAP = sum([v['AP'] for k, v in ret.items() if k in gt_classes_only]) / len(gt_classes_only) return {'per_class': ret, 'mAP': mAP}
def airports_within_radius(self): bounding_box = BoundingBox(self.point, self.radius) self.filter_circle(self.query.airports_inside(bounding_box.list()))