def test_get_local_path(self, mock_get_cache_dir): mock_get_cache_dir.return_value = self._cache_dir local_path = PathManager.get_local_path(self._remote_uri) self.assertTrue(os.path.exists(local_path)) self.assertTrue(os.path.isfile(local_path))
def __init__( self, dataset_name, tasks, distributed, output_dir=None, *, use_fast_impl=False, kpt_oks_sigmas=(), ): """ Args: dataset_name (str): name of the dataset to be evaluated. It must have either the following corresponding metadata: "json_file": the path to the COCO format annotation Or it must be in detectron2's standard dataset format so it can be converted to COCO format automatically. tasks (tuple[str]): tasks that can be evaluated under the given configuration. A task is one of "bbox", "segm", "keypoints". DEPRECATED pass cfgNode here to generate tasks from config distributed (True): if True, will collect results from all ranks and run evaluation in the main process. Otherwise, will only evaluate the results in the current process. output_dir (str): optional, an output directory to dump all results predicted on the dataset. The dump contains two files: 1. "instance_predictions.pth" a file in torch serialization format that contains all the raw original predictions. 2. "coco_instances_results.json" a json file in COCO's result format. use_fast_impl (bool): use a fast but **unofficial** implementation to compute AP. Although the results should be very close to the official implementation in COCO API, it is still recommended to compute results with the official API for use in papers. The faster implementation also uses more RAM. kpt_oks_sigmas (list[float]): The sigmas used to calculate keypoint OKS. See http://cocodataset.org/#keypoints-eval When empty, it will use the defaults in COCO. Otherwise it should be the same length as ROI_KEYPOINT_HEAD.NUM_KEYPOINTS. """ self._logger = logging.getLogger(__name__) if isinstance(tasks, CfgNode): kpt_oks_sigmas = ( tasks.TEST.KEYPOINT_OKS_SIGMAS if not kpt_oks_sigmas else kpt_oks_sigmas ) self._tasks = self._tasks_from_config(tasks) self._logger.warn( "COCO Evaluator instantiated using config, this is deprecated behavior." " Please pass tasks in directly" ) else: self._tasks = tasks self._distributed = distributed self._output_dir = output_dir self._use_fast_impl = use_fast_impl self._cpu_device = torch.device("cpu") self._metadata = MetadataCatalog.get(dataset_name) if not hasattr(self._metadata, "json_file"): self._logger.info( f"'{dataset_name}' is not registered by `register_coco_instances`." " Therefore trying to convert it to COCO format ..." ) cache_path = os.path.join(output_dir, f"{dataset_name}_coco_format.json") self._metadata.json_file = cache_path convert_to_coco_json(dataset_name, cache_path) json_file = PathManager.get_local_path(self._metadata.json_file) with contextlib.redirect_stdout(io.StringIO()): self._coco_api = COCO(json_file) self._kpt_oks_sigmas = kpt_oks_sigmas # Test set json files do not contain annotations (evaluation must be # performed using the COCO evaluation server). self._do_evaluation = "annotations" in self._coco_api.dataset
import logging import os import shutil from typing import List, Optional logger = logging.getLogger(__file__) try: from fvcore.common.file_io import PathManager as FVCorePathManager try: # [FB only - for now] AWS PathHandler for PathManager from .fb_pathhandlers import S3PathHandler FVCorePathManager.register_handler(S3PathHandler()) except KeyError: logging.warning("S3PathHandler already registered.") except ImportError: logging.debug( "S3PathHandler couldn't be imported. Either missing fb-only files, or boto3 module." ) except ImportError: FVCorePathManager = None class PathManager: """ Wrapper for insulating OSS I/O (using Python builtin operations) from fvcore's PathManager abstraction (for transparently handling various
def test_open(self) -> None: # pyre-ignore with PathManager.open(self._tmpfile, "r") as f: self.assertEqual(f.read(), self._tmpfile_contents)
def test_exists(self) -> None: # pyre-ignore self.assertTrue(PathManager.exists(self._tmpfile)) # pyre-ignore fake_path = os.path.join(self._tmpdir, uuid.uuid4().hex) self.assertFalse(PathManager.exists(fake_path))
def test_open(self) -> None: with self._patch_download(): with PathManager.open(self._remote_uri, "rb") as f: self.assertTrue(os.path.exists(f.name)) self.assertTrue(os.path.isfile(f.name)) self.assertTrue(f.read() != "")
def test_bad_args(self) -> None: with self.assertRaises(NotImplementedError): PathManager.copy( self._remote_uri, self._remote_uri, foo="foo" # type: ignore ) with self.assertRaises(NotImplementedError): PathManager.exists(self._remote_uri, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.get_local_path( self._remote_uri, foo="foo" # type: ignore ) with self.assertRaises(NotImplementedError): PathManager.isdir(self._remote_uri, foo="foo") # type: ignore with self.assertRaises(NotImplementedError): PathManager.isfile(self._remote_uri, foo="foo") # type: ignore with self.assertRaises(NotImplementedError): PathManager.ls(self._remote_uri, foo="foo") # type: ignore with self.assertRaises(NotImplementedError): PathManager.mkdirs(self._remote_uri, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.open(self._remote_uri, foo="foo") # type: ignore with self.assertRaises(NotImplementedError): PathManager.rm(self._remote_uri, foo="foo") # type: ignore PathManager.set_strict_kwargs_checking(False) PathManager.get_local_path(self._remote_uri, foo="foo") # type: ignore f = PathManager.open(self._remote_uri, foo="foo") # type: ignore f.close() PathManager.set_strict_kwargs_checking(True)
def setup_logger( output=None, distributed_rank=0, *, color=True, name="detectron2", abbrev_name=None ): """ Initialize the detectron2 logger and set its verbosity level to "DEBUG". Args: output (str): a file name or a directory to save log. If None, will not save log file. If ends with ".txt" or ".log", assumed to be a file name. Otherwise, logs will be saved to `output/log.txt`. name (str): the root module name of this logger abbrev_name (str): an abbreviation of the module, to avoid long names in logs. Set to "" to not log the root module in logs. By default, will abbreviate "detectron2" to "d2" and leave other modules unchanged. Returns: logging.Logger: a logger """ logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) logger.propagate = False if abbrev_name is None: abbrev_name = "d2" if name == "detectron2" else name plain_formatter = logging.Formatter( "[%(asctime)s] %(name)s %(levelname)s: %(message)s", datefmt="%m/%d %H:%M:%S" ) # stdout logging: master only if distributed_rank == 0: ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.DEBUG) if color: formatter = _ColorfulFormatter( colored("[%(asctime)s %(name)s]: ", "green") + "%(message)s", datefmt="%m/%d %H:%M:%S", root_name=name, abbrev_name=str(abbrev_name), ) else: formatter = plain_formatter ch.setFormatter(formatter) logger.addHandler(ch) # file logging: all workers if output is not None: if output.endswith(".txt") or output.endswith(".log"): filename = output else: filename = os.path.join(output, "log.txt") if distributed_rank > 0: filename = filename + ".rank{}".format(distributed_rank) PathManager.mkdirs(os.path.dirname(filename)) fh = logging.StreamHandler(_cached_log_stream(filename)) fh.setLevel(logging.DEBUG) fh.setFormatter(plain_formatter) logger.addHandler(fh) return logger
def _cached_log_stream(filename): return PathManager.open(filename, "a")
def test_get_local_path(self): self.assertEqual(PathManager.get_local_path(self._tmpfile), self._tmpfile)
def test_exists(self): self.assertTrue(PathManager.exists(self._tmpfile)) fake_path = os.path.join(self._tmpdir, uuid.uuid4().hex) self.assertFalse(PathManager.exists(fake_path))
def test_open(self): with PathManager.open(self._tmpfile, "r") as f: self.assertEqual(f.read(), self._tmpfile_contents)
def test_PathManager(self): x = LazyPath(lambda: "./") output = PathManager.ls(x) output_gt = PathManager.ls("./") self.assertEqual(sorted(output), sorted(output_gt))
def test_open(self, mock_get_cache_dir): mock_get_cache_dir.return_value = self._cache_dir with PathManager.open(self._remote_uri, "rb") as f: self.assertTrue(os.path.exists(f.name)) self.assertTrue(os.path.isfile(f.name)) self.assertTrue(f.read() != "")
def test_bad_args(self) -> None: # TODO (T58240718): Replace with dynamic checks with self.assertRaises(ValueError): PathManager.copy( self._tmpfile, self._tmpfile, foo="foo" # type: ignore ) with self.assertRaises(ValueError): PathManager.exists(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.get_local_path(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.isdir(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.isfile(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.ls(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.mkdirs(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.open(self._tmpfile, foo="foo") # type: ignore with self.assertRaises(ValueError): PathManager.rm(self._tmpfile, foo="foo") # type: ignore PathManager.set_strict_kwargs_checking(False) PathManager.copy( self._tmpfile, self._tmpfile, foo="foo" # type: ignore ) PathManager.exists(self._tmpfile, foo="foo") # type: ignore PathManager.get_local_path(self._tmpfile, foo="foo") # type: ignore PathManager.isdir(self._tmpfile, foo="foo") # type: ignore PathManager.isfile(self._tmpfile, foo="foo") # type: ignore PathManager.ls(self._tmpdir, foo="foo") # type: ignore PathManager.mkdirs(self._tmpdir, foo="foo") # type: ignore f = PathManager.open(self._tmpfile, foo="foo") # type: ignore f.close() # pyre-ignore with open(os.path.join(self._tmpdir, "test_rm.txt"), "w") as f: rm_file = f.name f.write(self._tmpfile_contents) f.flush() PathManager.rm(rm_file, foo="foo") # type: ignore
def viz_preds(cfg) -> List[wandb.Image]: """Returns a sample of image predictions and its corresponding groundtruth. Parameters ---------- cfg : Returns ------- List[wandb.Image] """ output_path = Path(cfg.OUTPUT_DIR) # Requires JSON predictions file predictions_path = output_path / "coco_instances_results.json" val_dataset = cfg.DATASETS.TEST[0] # To filter out predictions conf_threshold = 0.1 # Load predictions JSON file with PathManager.open(str(predictions_path), "r") as f: # List of instance predictions predictions = json.load(f) # Group predictions for each image # i.e. image_id -> List[predictions] pred_by_image = defaultdict(list) for p in predictions: pred_by_image[p["image_id"]].append(p) # Get groundtruth annotations dicts = list(DatasetCatalog.get(val_dataset)) metadata = MetadataCatalog.get(val_dataset) # Sample images to visualize imgs = [] n_imgs = 8 dicts = sample(dicts, n_imgs) for dic in tqdm(dicts): img = cv2.imread(dic["file_name"], cv2.IMREAD_COLOR)[:, :, ::-1] # Creates Instances object predictions = pred_by_image[dic["image_id"]] predictions = create_instances( predictions, img.shape[:2], metadata, conf_threshold ) # Draw instance-level predictions on an image vis = Visualizer(img, metadata) vis_pred = vis.draw_instance_predictions(predictions).get_image() # Draw ground-truth annotations on an image vis = Visualizer(img, metadata) vis_gt = vis.draw_dataset_dict(dic).get_image() # Place them side by side concat = np.concatenate((vis_pred, vis_gt), axis=1) # For wandb logging imgs.append(wandb.Image(concat)) return imgs
def test_get_local_path(self) -> None: with self._patch_download(): local_path = PathManager.get_local_path(self._remote_uri) self.assertTrue(os.path.exists(local_path)) self.assertTrue(os.path.isfile(local_path))
def _eval_predictions(self): """ Evaluate self._predictions on the instance detection task. Fill self._results with the metrics of the instance detection task. """ self._logger.info("Preparing results for COCO format ...") self._coco_results = list( itertools.chain(*[x["instances"] for x in self._predictions])) # unmap the category ids for COCO if hasattr(self._metadata, "thing_dataset_id_to_contiguous_id"): reverse_id_mapping = { v: k for k, v in self._metadata.thing_dataset_id_to_contiguous_id.items() } for result in self._coco_results: result["category_id"] = reverse_id_mapping[ result["category_id"]] if self._output_dir: file_path = os.path.join(self._output_dir, "coco_instances_results.json") self._logger.info("Saving results to {}".format(file_path)) with PathManager.open(file_path, "w") as f: f.write(json.dumps(self._coco_results)) f.flush() if not self._do_evaluation: self._logger.info("Annotations are not available for evaluation.") return self._logger.info("Evaluating predictions ...") if self._is_splits: self._results["bbox"] = {} for split, classes, names in [ ("all", None, self._metadata.get("thing_classes")), ("base", self._base_classes, self._metadata.get("base_classes")), ("novel", self._novel_classes, self._metadata.get("novel_classes")) ]: if "all" not in self._dataset_name and \ split not in self._dataset_name: continue coco_eval = ( _evaluate_predictions_on_coco( self._coco_api, self._coco_results, "bbox", classes, ) if len(self._coco_results) > 0 else None # cocoapi does not handle empty results very well ) res_ = self._derive_coco_results( coco_eval, "bbox", class_names=names, ) res = {} for metric in res_.keys(): if len(metric) <= 4: if split == "all": res[metric] = res_[metric] elif split == "base": res["b" + metric] = res_[metric] elif split == "novel": res["n" + metric] = res_[metric] self._results["bbox"].update(res) # add "AP" if not already in if "AP" not in self._results["bbox"]: if "nAP" in self._results["bbox"]: self._results["bbox"]["AP"] = self._results["bbox"]["nAP"] else: self._results["bbox"]["AP"] = self._results["bbox"]["bAP"] else: coco_eval = ( _evaluate_predictions_on_coco( self._coco_api, self._coco_results, "bbox", ) if len(self._coco_results) > 0 else None # cocoapi does not handle empty results very well ) res = self._derive_coco_results( coco_eval, "bbox", class_names=self._metadata.get("thing_classes")) self._results["bbox"] = res
def test_open_writes(self) -> None: # HTTPURLHandler does not support writing, only reading. with self.assertRaises(AssertionError): with PathManager.open(self._remote_uri, "w") as f: f.write("foobar") # pyre-ignore
def __init__(self, dataset_name, cfg, distributed, output_dir=None): """ Args: dataset_name (str): name of the dataset to be evaluated. It must have either the following corresponding metadata: "json_file": the path to the COCO format annotation Or it must be in detectron2's standard dataset format so it can be converted to COCO format automatically. cfg (CfgNode): config instance distributed (True): if True, will collect results from all ranks for evaluation. Otherwise, will evaluate the results in the current process. output_dir (str): optional, an output directory to dump results. """ self._distributed = distributed self._output_dir = output_dir self._dataset_name = dataset_name self._cpu_device = torch.device("cpu") self._logger = logging.getLogger(__name__) self._metadata = MetadataCatalog.get(dataset_name) if not hasattr(self._metadata, "json_file"): self._logger.warning( f"json_file was not found in MetaDataCatalog for '{dataset_name}'" ) cache_path = convert_to_coco_json(dataset_name, output_dir) self._metadata.json_file = cache_path self._is_splits = "all" in dataset_name or "base" in dataset_name \ or "novel" in dataset_name self._base_classes = [ 8, 10, 11, 13, 14, 15, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, ] self._novel_classes = [ 1, 2, 3, 4, 5, 6, 7, 9, 16, 17, 18, 19, 20, 21, 44, 62, 63, 64, 67, 72 ] json_file = PathManager.get_local_path(self._metadata.json_file) with contextlib.redirect_stdout(io.StringIO()): self._coco_api = COCO(json_file) # Test set json files do not contain annotations (evaluation must be # performed using the COCO evaluation server). self._do_evaluation = "annotations" in self._coco_api.dataset
def test_PathManager(self) -> None: x = LazyPath(lambda: "./") output = PathManager.ls(x) # pyre-ignore output_gt = PathManager.ls("./") self.assertEqual(sorted(output), sorted(output_gt))
def get_class_names(path, parent_path=None, subset_path=None): """ Read json file with entries {classname: index} and return an array of class names in order. If parent_path is provided, load and map all children to their ids. Args: path (str): path to class ids json file. File must be in the format {"class1": id1, "class2": id2, ...} parent_path (Optional[str]): path to parent-child json file. File must be in the format {"parent1": ["child1", "child2", ...], ...} subset_path (Optional[str]): path to text file containing a subset of class names, separated by newline characters. Returns: class_names (list of strs): list of class names. class_parents (dict): a dictionary where key is the name of the parent class and value is a list of ids of the children classes. subset_ids (list of ints): list of ids of the classes provided in the subset file. """ try: with PathManager.open(path, "r") as f: class2idx = json.load(f) except Exception as err: print("Fail to load file from {} with error {}".format(path, err)) return class_names = [None] * len(class2idx) for k, i in class2idx.items(): class_names[i] = k class_parent = None if parent_path is not None and parent_path != "": try: with PathManager.open(parent_path, "r") as f: d_parent = json.load(f) except EnvironmentError as err: print("Fail to load file from {} with error {}".format( parent_path, err)) return class_parent = {} for parent, children in d_parent.items(): indices = [ class2idx[c] for c in children if class2idx.get(c) is not None ] class_parent[parent] = indices subset_ids = None if subset_path is not None and subset_path != "": try: with PathManager.open(subset_path, "r") as f: subset = f.read().split("\n") subset_ids = [ class2idx[name] for name in subset if class2idx.get(name) is not None ] except EnvironmentError as err: print("Fail to load file from {} with error {}".format( subset_path, err)) return return class_names, class_parent, subset_ids
def test_get_local_path(self) -> None: self.assertEqual( # pyre-ignore PathManager.get_local_path(self._tmpfile), self._tmpfile, )
def cityscapes_files_to_dict(files, from_json, to_polygons): """ Parse cityscapes annotation files to a dict. Args: files (tuple): consists of (image_file, instance_id_file, label_id_file, json_file) from_json (bool): whether to read annotations from the raw json file or the png files. to_polygons (bool): whether to represent the segmentation as polygons (COCO's format) instead of masks (cityscapes's format). Returns: A dict in Detectron2 Dataset format. """ from cityscapesscripts.helpers.labels import id2label, name2label image_file, instance_id_file, _, json_file = files annos = [] if from_json: from shapely.geometry import MultiPolygon, Polygon with PathManager.open(json_file, "r") as f: jsonobj = json.load(f) ret = { "file_name": image_file, "image_id": os.path.basename(image_file), "height": jsonobj["imgHeight"], "width": jsonobj["imgWidth"], } # `polygons_union` contains the union of all valid polygons. polygons_union = Polygon() # CityscapesScripts draw the polygons in sequential order # and each polygon *overwrites* existing ones. See # (https://github.com/mcordts/cityscapesScripts/blob/master/cityscapesscripts/preparation/json2instanceImg.py) # noqa # We use reverse order, and each polygon *avoids* early ones. # This will resolve the ploygon overlaps in the same way as CityscapesScripts. for obj in jsonobj["objects"][::-1]: if "deleted" in obj: # cityscapes data format specific continue label_name = obj["label"] try: label = name2label[label_name] except KeyError: if label_name.endswith("group"): # crowd area label = name2label[label_name[:-len("group")]] else: raise if label.id < 0: # cityscapes data format continue # Cityscapes's raw annotations uses integer coordinates # Therefore +0.5 here poly_coord = np.asarray(obj["polygon"], dtype="f4") + 0.5 # CityscapesScript uses PIL.ImageDraw.polygon to rasterize # polygons for evaluation. This function operates in integer space # and draws each pixel whose center falls into the polygon. # Therefore it draws a polygon which is 0.5 "fatter" in expectation. # We therefore dilate the input polygon by 0.5 as our input. poly = Polygon(poly_coord).buffer(0.5, resolution=4) if not label.hasInstances or label.ignoreInEval: # even if we won't store the polygon it still contributes to overlaps resolution polygons_union = polygons_union.union(poly) continue # Take non-overlapping part of the polygon poly_wo_overlaps = poly.difference(polygons_union) if poly_wo_overlaps.is_empty: continue polygons_union = polygons_union.union(poly) anno = {} anno["iscrowd"] = label_name.endswith("group") anno["category_id"] = label.id if isinstance(poly_wo_overlaps, Polygon): poly_list = [poly_wo_overlaps] elif isinstance(poly_wo_overlaps, MultiPolygon): poly_list = poly_wo_overlaps.geoms else: raise NotImplementedError( "Unknown geometric structure {}".format(poly_wo_overlaps)) poly_coord = [] for poly_el in poly_list: # COCO API can work only with exterior boundaries now, hence we store only them. # TODO: store both exterior and interior boundaries once other parts of the # codebase support holes in polygons. poly_coord.append(list(chain(*poly_el.exterior.coords))) anno["segmentation"] = poly_coord (xmin, ymin, xmax, ymax) = poly_wo_overlaps.bounds anno["bbox"] = (xmin, ymin, xmax, ymax) anno["bbox_mode"] = BoxMode.XYXY_ABS annos.append(anno) else: # See also the official annotation parsing scripts at # https://github.com/mcordts/cityscapesScripts/blob/master/cityscapesscripts/evaluation/instances2dict.py # noqa with PathManager.open(instance_id_file, "rb") as f: inst_image = np.asarray(Image.open(f), order="F") # ids < 24 are stuff labels (filtering them first is about 5% faster) flattened_ids = np.unique(inst_image[inst_image >= 24]) ret = { "file_name": image_file, "image_id": os.path.basename(image_file), "height": inst_image.shape[0], "width": inst_image.shape[1], } for instance_id in flattened_ids: # For non-crowd annotations, instance_id // 1000 is the label_id # Crowd annotations have <1000 instance ids label_id = instance_id // 1000 if instance_id >= 1000 else instance_id label = id2label[label_id] if not label.hasInstances or label.ignoreInEval: continue anno = {} anno["iscrowd"] = instance_id < 1000 anno["category_id"] = label.id mask = np.asarray(inst_image == instance_id, dtype=np.uint8, order="F") inds = np.nonzero(mask) ymin, ymax = inds[0].min(), inds[0].max() xmin, xmax = inds[1].min(), inds[1].max() anno["bbox"] = (xmin, ymin, xmax, ymax) if xmax <= xmin or ymax <= ymin: continue anno["bbox_mode"] = BoxMode.XYXY_ABS if to_polygons: # This conversion comes from D4809743 and D5171122, # when Mask-RCNN was first developed. contours = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] polygons = [ c.reshape(-1).tolist() for c in contours if len(c) >= 3 ] # opencv's can produce invalid polygons if len(polygons) == 0: continue anno["segmentation"] = polygons else: anno["segmentation"] = mask_util.encode(mask[:, :, None])[0] annos.append(anno) ret["annotations"] = annos return ret
def load_vg_json(json_file, image_root, dataset_name=None, extra_annotation_keys=None): """ Load a json file with COCO's instances annotation format. Currently supports instance detection, instance segmentation, and person keypoints annotations. Args: json_file (str): full path to the json file in COCO instances annotation format. image_root (str): the directory where the images in this json file exists. dataset_name (str): the name of the dataset (e.g., coco_2017_train). If provided, this function will also put "thing_classes" into the metadata associated with this dataset. extra_annotation_keys (list[str]): list of per-annotation keys that should also be loaded into the dataset dict (besides "iscrowd", "bbox", "keypoints", "category_id", "segmentation"). The values for these keys will be returned as-is. For example, the densepose annotations are loaded in this way. Returns: list[dict]: a list of dicts in Detectron2 standard format. (See `Using Custom Datasets </tutorials/datasets.html>`_ ) Notes: 1. This function does not read the image files. The results do not have the "image" field. """ from pycocotools.coco import COCO timer = Timer() json_file = PathManager.get_local_path(json_file) with contextlib.redirect_stdout(io.StringIO()): coco_api = COCO(json_file) if timer.seconds() > 1: logger.info("Loading {} takes {:.2f} seconds.".format(json_file, timer.seconds())) id_map = None if dataset_name is not None: meta = MetadataCatalog.get(dataset_name) cat_ids = sorted(coco_api.getCatIds()) cats = coco_api.loadCats(cat_ids) # The categories in a custom json file may not be sorted. thing_classes = [c["name"] for c in sorted(cats, key=lambda x: x["id"])] meta.thing_classes = thing_classes # In COCO, certain category ids are artificially removed, # and by convention they are always ignored. # We deal with COCO's id issue and translate # the category ids to contiguous ids in [0, 80). # It works by looking at the "categories" field in the json, therefore # if users' own json also have incontiguous ids, we'll # apply this mapping as well but print a warning. if not (min(cat_ids) == 1 and max(cat_ids) == len(cat_ids)): if "coco" not in dataset_name: logger.warning( """ Category ids in annotations are not in [1, #categories]! We'll apply a mapping for you. """ ) id_map = {v: i for i, v in enumerate(cat_ids)} meta.thing_dataset_id_to_contiguous_id = id_map # sort indices for reproducible results img_ids = sorted(list(coco_api.imgs.keys())) # imgs is a list of dicts, each looks something like: # {'license': 4, # 'url': 'http://farm6.staticflickr.com/5454/9413846304_881d5e5c3b_z.jpg', # 'file_name': 'COCO_val2014_000000001268.jpg', # 'height': 427, # 'width': 640, # 'date_captured': '2013-11-17 05:57:24', # 'id': 1268} imgs = coco_api.loadImgs(img_ids) # anns is a list[list[dict]], where each dict is an annotation # record for an object. The inner list enumerates the objects in an image # and the outer list enumerates over images. Example of anns[0]: # [{'segmentation': [[192.81, # 247.09, # ... # 219.03, # 249.06]], # 'area': 1035.749, # 'iscrowd': 0, # 'image_id': 1268, # 'bbox': [192.81, 224.8, 74.73, 33.43], # 'category_id': 16, # 'id': 42986}, # ...] anns = [coco_api.imgToAnns[img_id] for img_id in img_ids] if "minival" not in json_file: # The popular valminusminival & minival annotations for COCO2014 contain this bug. # However the ratio of buggy annotations there is tiny and does not affect accuracy. # Therefore we explicitly white-list them. ann_ids = [ann["id"] for anns_per_image in anns for ann in anns_per_image] assert len(set(ann_ids)) == len(ann_ids), "Annotation ids in '{}' are not unique!".format( json_file ) imgs_anns = list(zip(imgs, anns)) logger.info("Loaded {} images in COCO format from {}".format(len(imgs_anns), json_file)) dataset_dicts = [] ann_keys = ["iscrowd", "bbox", "keypoints", "category_id"] + (extra_annotation_keys or []) num_instances_without_valid_segmentation = 0 max_attributes_per_ins = 16 for (img_dict, anno_dict_list) in imgs_anns: record = {} record["file_name"] = os.path.join(image_root, img_dict["file_name"]) record["height"] = img_dict["height"] record["width"] = img_dict["width"] image_id = record["image_id"] = img_dict["id"] objs = [] for anno in anno_dict_list: # Check that the image_id in this annotation is the same as # the image_id we're looking at. # This fails only when the data parsing logic or the annotation file is buggy. # The original COCO valminusminival2014 & minival2014 annotation files # actually contains bugs that, together with certain ways of using COCO API, # can trigger this assertion. assert anno["image_id"] == image_id assert anno.get("ignore", 0) == 0 obj = {key: anno[key] for key in ann_keys if key in anno} attributes = [-1 for _ in range(max_attributes_per_ins)] attr = anno.get("attribute", None) if attr: attributes = [-1 for _ in range(max_attributes_per_ins)] for idx, a in enumerate(attr): attributes[idx] = a obj["attributes"] = attributes segm = anno.get("segmentation", None) if segm: # either list[list[float]] or dict(RLE) if not isinstance(segm, dict): # filter out invalid polygons (< 3 points) segm = [poly for poly in segm if len(poly) % 2 == 0 and len(poly) >= 6] if len(segm) == 0: num_instances_without_valid_segmentation += 1 continue # ignore this instance obj["segmentation"] = segm keypts = anno.get("keypoints", None) if keypts: # list[int] for idx, v in enumerate(keypts): if idx % 3 != 2: # COCO's segmentation coordinates are floating points in [0, H or W], # but keypoint coordinates are integers in [0, H-1 or W-1] # Therefore we assume the coordinates are "pixel indices" and # add 0.5 to convert to floating point coordinates. keypts[idx] = v + 0.5 obj["keypoints"] = keypts obj["bbox_mode"] = BoxMode.XYWH_ABS if id_map: obj["category_id"] = id_map[obj["category_id"]] objs.append(obj) record["annotations"] = objs dataset_dicts.append(record) if num_instances_without_valid_segmentation > 0: logger.warn( "Filtered out {} instances without valid segmentation. " "There might be issues in your dataset generation process.".format( num_instances_without_valid_segmentation ) ) return dataset_dicts
def load_sem_seg(gt_root, image_root, gt_ext="png", image_ext="jpg"): """ Load semantic segmentation datasets. All files under "gt_root" with "gt_ext" extension are treated as ground truth annotations and all files under "image_root" with "image_ext" extension as input images. Ground truth and input images are matched using file paths relative to "gt_root" and "image_root" respectively without taking into account file extensions. This works for COCO as well as some other datasets. Args: gt_root (str): full path to ground truth semantic segmentation files. Semantic segmentation annotations are stored as images with integer values in pixels that represent corresponding semantic labels. image_root (str): the directory where the input images are. gt_ext (str): file extension for ground truth annotations. image_ext (str): file extension for input images. Returns: list[dict]: a list of dicts in detectron2 standard format without instance-level annotation. Notes: 1. This function does not read the image and ground truth files. The results do not have the "image" and "sem_seg" fields. """ # We match input images with ground truth based on their relative filepaths (without file # extensions) starting from 'image_root' and 'gt_root' respectively. def file2id(folder_path, file_path): # extract relative path starting from `folder_path` image_id = os.path.normpath(os.path.relpath(file_path, start=folder_path)) # remove file extension image_id = os.path.splitext(image_id)[0] return image_id input_files = sorted( (os.path.join(image_root, f) for f in PathManager.ls(image_root) if f.endswith(image_ext)), key=lambda file_path: file2id(image_root, file_path), ) gt_files = sorted( (os.path.join(gt_root, f) for f in PathManager.ls(gt_root) if f.endswith(gt_ext)), key=lambda file_path: file2id(gt_root, file_path), ) assert len(gt_files) > 0, "No annotations found in {}.".format(gt_root) # Use the intersection, so that val2017_100 annotations can run smoothly with val2017 images if len(input_files) != len(gt_files): logger.warn( "Directory {} and {} has {} and {} files, respectively.".format( image_root, gt_root, len(input_files), len(gt_files) ) ) input_basenames = [os.path.basename(f)[: -len(image_ext)] for f in input_files] gt_basenames = [os.path.basename(f)[: -len(gt_ext)] for f in gt_files] intersect = list(set(input_basenames) & set(gt_basenames)) # sort, otherwise each worker may obtain a list[dict] in different order intersect = sorted(intersect) logger.warn("Will use their intersection of {} files.".format(len(intersect))) input_files = [os.path.join(image_root, f + image_ext) for f in intersect] gt_files = [os.path.join(gt_root, f + gt_ext) for f in intersect] logger.info( "Loaded {} images with semantic segmentation from {}".format(len(input_files), image_root) ) dataset_dicts = [] for (img_path, gt_path) in zip(input_files, gt_files): record = {} record["file_name"] = img_path record["sem_seg_file_name"] = gt_path dataset_dicts.append(record) return dataset_dicts
def __call__(self, dataset_dict): """ Args: dataset_dict (dict): Metadata of one image, in centernet Dataset format. Returns: dict: a format that builtin models in centernet accept """ dataset_dict = copy.deepcopy(dataset_dict) # it will be modified by code below # USER: Write your own image loading if it's not from a file image = utils.read_image(dataset_dict["file_name"], format=self.img_format) utils.check_image_size(dataset_dict, image) if "annotations" not in dataset_dict: image, transforms = T.apply_transform_gens( ([self.crop_gen] if self.crop_gen else []) + self.tfm_gens, image ) else: # Crop around an instance if there are instances in the image. # USER: Remove if you don't use cropping if self.crop_gen: crop_tfm = utils.gen_crop_transform_with_instance( self.crop_gen.get_crop_size(image.shape[:2]), image.shape[:2], np.random.choice(dataset_dict["annotations"]), ) image = crop_tfm.apply_image(image) image, transforms = T.apply_transform_gens(self.tfm_gens, image) if self.crop_gen: transforms = crop_tfm + transforms image_shape = image.shape[:2] # h, w # Pytorch's dataloader is efficient on torch.Tensor due to shared-memory, # but not efficient on large generic data structures due to the use of pickle & mp.Queue. # Therefore it's important to use torch.Tensor. dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) # Can use uint8 if it turns out to be slow some day # USER: Remove if you don't use pre-computed proposals. if self.load_proposals: utils.transform_proposals( dataset_dict, image_shape, transforms, self.min_box_side_len, self.proposal_topk ) if not self.is_train and not self.eval_with_gt: dataset_dict.pop("annotations", None) dataset_dict.pop("sem_seg_file_name", None) return dataset_dict if "annotations" in dataset_dict: # USER: Modify this if you want to keep them for some reason. for anno in dataset_dict["annotations"]: if not self.mask_on: anno.pop("segmentation", None) if not self.keypoint_on: anno.pop("keypoints", None) # USER: Implement additional transformations if you have other types of data annos = [ utils.transform_instance_annotations( obj, transforms, image_shape, keypoint_hflip_indices=self.keypoint_hflip_indices ) for obj in dataset_dict.pop("annotations") if obj.get("iscrowd", 0) == 0 ] instances = utils.annotations_to_instances( annos, image_shape, mask_format=self.mask_format ) # Create a tight bounding box from masks, useful when image is cropped if self.crop_gen and instances.has("gt_masks"): instances.gt_boxes = instances.gt_masks.get_bounding_boxes() dataset_dict["instances"] = utils.filter_empty_instances(instances) # USER: Remove if you don't do semantic/panoptic segmentation. if "sem_seg_file_name" in dataset_dict: with PathManager.open(dataset_dict.pop("sem_seg_file_name"), "rb") as f: sem_seg_gt = Image.open(f) sem_seg_gt = np.asarray(sem_seg_gt, dtype="uint8") sem_seg_gt = transforms.apply_segmentation(sem_seg_gt) sem_seg_gt = torch.as_tensor(sem_seg_gt.astype("long")) dataset_dict["sem_seg"] = sem_seg_gt return dataset_dict
def test_mkdirs(self) -> None: # pyre-ignore new_dir_path = os.path.join(self._tmpdir, "new", "tmp", "dir") self.assertFalse(PathManager.exists(new_dir_path)) PathManager.mkdirs(new_dir_path) self.assertTrue(PathManager.exists(new_dir_path))
def rm(path: str) -> None: if FVCorePathManager: return FVCorePathManager.rm(path) os.remove(path)
def load_filtered_voc_instances(name: str, dirname: str, split: str, classnames: str): """ Load Pascal VOC detection annotations to Detectron2 format. Args: dirname: Contain "Annotations", "ImageSets", "JPEGImages" split (str): one of "train", "test", "val", "trainval" """ is_shots = "shot" in name if is_shots: fileids = {} split_dir = os.path.join("datasets", "vocsplit") if "seed" in name: shot = name.split('_')[-2].split('shot')[0] seed = int(name.split('_seed')[-1]) split_dir = os.path.join(split_dir, "seed{}".format(seed)) else: shot = name.split('_')[-1].split('shot')[0] for cls in classnames: with PathManager.open( os.path.join(split_dir, "box_{}shot_{}_train.txt".format(shot, cls))) as f: fileids_ = np.loadtxt(f, dtype=np.str).tolist() if isinstance(fileids_, str): fileids_ = [fileids_] fileids_ = [fid.split('/')[-1].split('.jpg')[0] \ for fid in fileids_] fileids[cls] = fileids_ else: with PathManager.open( os.path.join(dirname, "ImageSets", "Main", split + ".txt")) as f: fileids = np.loadtxt(f, dtype=np.str) dicts = [] if is_shots: for cls, fileids_ in fileids.items(): dicts_ = [] for fileid in fileids_: year = "2012" if "_" in fileid else "2007" dirname = os.path.join("datasets", "VOC{}".format(year)) anno_file = os.path.join(dirname, "Annotations", fileid + ".xml") jpeg_file = os.path.join(dirname, "JPEGImages", fileid + ".jpg") tree = ET.parse(anno_file) for obj in tree.findall("object"): r = { "file_name": jpeg_file, "image_id": fileid, "height": int(tree.findall("./size/height")[0].text), "width": int(tree.findall("./size/width")[0].text), } cls_ = obj.find("name").text if cls != cls_: continue bbox = obj.find("bndbox") bbox = [ float(bbox.find(x).text) for x in ["xmin", "ymin", "xmax", "ymax"] ] bbox[0] -= 1.0 bbox[1] -= 1.0 instances = [{ "category_id": classnames.index(cls), "bbox": bbox, "bbox_mode": BoxMode.XYXY_ABS }] r["annotations"] = instances dicts_.append(r) if len(dicts_) > int(shot): dicts_ = np.random.choice(dicts_, int(shot), replace=False) dicts.extend(dicts_) else: for fileid in fileids: anno_file = os.path.join(dirname, "Annotations", fileid + ".xml") jpeg_file = os.path.join(dirname, "JPEGImages", fileid + ".jpg") tree = ET.parse(anno_file) r = { "file_name": jpeg_file, "image_id": fileid, "height": int(tree.findall("./size/height")[0].text), "width": int(tree.findall("./size/width")[0].text), } instances = [] for obj in tree.findall("object"): cls = obj.find("name").text if not (cls in classnames): continue bbox = obj.find("bndbox") bbox = [ float(bbox.find(x).text) for x in ["xmin", "ymin", "xmax", "ymax"] ] bbox[0] -= 1.0 bbox[1] -= 1.0 instances.append({ "category_id": classnames.index(cls), "bbox": bbox, "bbox_mode": BoxMode.XYXY_ABS, }) r["annotations"] = instances dicts.append(r) return dicts