Skip to content

Commit

Permalink
Add vis2yolo utils, disp nb
Browse files Browse the repository at this point in the history
  • Loading branch information
SamSamhuns committed Apr 19, 2023
1 parent 1fd419c commit 94be9ce
Show file tree
Hide file tree
Showing 13 changed files with 4,278 additions and 0 deletions.
84 changes: 84 additions & 0 deletions adv_patch_gen/conv_visdrone_2_yolo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Adversarial Patches against UAE Object Detection

## Setup

Tested with python 3.8

```shell
python -m venv venv
source venv/bin/activate
pip install tqdm==4.65.0
pip install imagesize==1.4.1
pip install opencv-python==4.7.0.72
```

## VisDrone Dataset Format

Dataset can be downloaded from <https://github.com/VisDrone/VisDrone-Dataset>.

Annotations for the detections are follows:

`<bbox_left>, <bbox_top>, <bbox_width>, <bbox_height>, <score>, <object_category>, <truncation>, <occlusion>`

- <bbox_left> The x coordinate of the top-left corner of the predicted bounding box
- <bbox_top> The y coordinate of the top-left corner of the predicted object bounding box
- <bbox_width> The width in pixels of the predicted object bounding box
- <bbox_height> The height in pixels of the predicted object bounding box
- <score> The score in the DETECTION file indicates the confidence of the predicted bounding box enclosing an object instance. The score in GROUNDTRUTH file is set to 1 or 0. 1 indicates the bounding box is considered in evaluation, while 0 indicates the bounding box will be ignored.
- <object_category> The object category indicates the type of annotated object, (i.e., ignored regions(0), pedestrian(1), people(2), bicycle(3), car(4), van(5), truck(6), tricycle(7), awning-tricycle(8), bus(9), motor(10), others(11))
- <truncation> The score in the DETECTION result file should be set to the constant -1.The score in the GROUNDTRUTH file indicates the degree of object parts appears outside a frame (i.e., no truncation = 0 (truncation ratio 0%), and partial truncation = 1 (truncation ratio 1% ~ 50%)).
- <occlusion> The score in the DETECTION file should be set to the constant -1. The score in the GROUNDTRUTH file indicates the fraction of objects being occluded (i.e., no occlusion = 0 (occlusion ratio 0%), partial occlusion = 1 (occlusion ratio 1% ~ 50%), and heavy occlusion = 2 (occlusion ratio 50% ~ 100%)).

## Download VisDrone Dataset

Download Dataset from <https://github.com/VisDrone/VisDrone-Dataset> and unzip and place under top level directory `data`.

Alternatively, use gdown to download the zip files from the command line.

```shell
mkdir data
cd data
pip install gdown
# object detection train subset
gdown 1a2oHjcEcwXP8oUF95qiwrqzACb2YlUhn
# object detection val subset
gdown 1bxK5zgLn0_L8x276eKkuYA_FzwCIjb59
# object detection test subset
gdown 1PFdW_VFSCfZ_sTSZAGjQdifF_Xd5mf0V

# unzip all data
unzip VisDrone2019-DET-test-dev.zip
unzip VisDrone2019-DET-train.zip
unzip VisDrone2019-DET-val.zip

# remove all zip files
rm *.zip
```

## Visualize Images

Running annotation visualizations for VisDrone or YOLO format.

```shell
python disp_visdrone.py -a ANNOTS_DIR -i IMAGES_DIR
python disp_yolo.py -a ANNOTS_DIR -i IMAGES_DIR
```

## Convert VisDrone to YOLO annotation format

Note: The classes to consider and any additional re-assingment of classes must be done with variables `CLASS_2_CONSIDER` and `CLASS_ID_REMAP` inside `conv_visdrone_2_yolo_fmt.py`.

Can use optional params `low_dim_cutoff` and `low_area_cutoff` to cutoff bunding boxes that do not satisfy a minimum box dimenison or area percentage cutoff.

```shell
# example conversion of VisDrone train, val and test set annotations to YOLO format. Use -h for all options
python conv_visdrone_2_yolo_fmt.py --sad data/VisDrone2019-DET-train/annotations --sid data/VisDrone2019-DET-train/images --td data/VisDrone2019-DET-train/labels
python conv_visdrone_2_yolo_fmt.py --sad data/VisDrone2019-DET-val/annotations --sid data/VisDrone2019-DET-val/images --td data/VisDrone2019-DET-val/labels
python conv_visdrone_2_yolo_fmt.py --sad data/VisDrone2019-DET-test-dev/annotations --sid data/VisDrone2019-DET-test-dev/images --td data/VisDrone2019-DET-test-dev/labels
```

Note: To convert to COCO annotation format refer to <https://github.com/SamSamhuns/ml_data_processing/tree/master/annotation_format_conv>

## References

- [VisDrone](https://github.com/VisDrone/VisDrone-Dataset)
116 changes: 116 additions & 0 deletions adv_patch_gen/conv_visdrone_2_yolo/conv_visdrone_2_yolo_fmt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
Convert VisDrone annotation format to YOLO labels format
Works for training of YOLOv5 and YOLOv7.
YOLOv7 requires an additional txt file (Same name as the first parent directory) with paths to the images for the train, val & test splits
"""
import os
import os.path as osp
import glob
import argparse
from typing import Optional

import tqdm
import imagesize

# VisDrone annot fmt
# <bbox_left>, <bbox_top>, <bbox_width>, <bbox_height>, <score>, <object_category>, <truncation>, <occlusion>
#
# classes:
# ignore(0), pedestrian(1), people(2), bicycle(3),
# car(4), van(5), truck(6), tricycle(7),
# awning-tricycle(8), bus(9), motor(10), others(11)

# YOLO annot fmt
# One row per object
# Each row is class x_center y_center width height format.
# Box coordinates must be in normalized xywh format (from 0 - 1).
# If boxes are in pixels, divide x_center and width by image width, and y_center and height by image height.
# Class numbers are zero-indexed (start from 0).


CLASS_2_CONSIDER = {1, 2, 4, 5, 6, 9} # only get pedestrian, people, car, van, truck, bus classes from VisDrone
CLASS_ID_REMAP = {4: 0, 5: 0, 6: 1, 9: 2, 1: 3, 2: 3} # optionally remap class ids, can be set to None
IMG_EXT = {".jpg", ".png"}


def get_parsed_args():
parser = argparse.ArgumentParser(
description="VisDrone to YOLO annot format")
parser.add_argument('--sad', '--source_annot_dir', type=str, dest="source_annot_dir", required=True,
help='VisDrone annotation source dir. Should contain annot txt files (default: %(default)s)')
parser.add_argument('--sid', '--source_image_dir', type=str, dest="source_image_dir", required=True,
help='VisDrone images source dir. Should contain image files (default: %(default)s)')
parser.add_argument('--td', '--target_annot_dir', type=str, dest="target_annot_dir", required=True,
help='YOLO annotation target dir. YOLO by default uses dirname labels (default: %(default)s)')
parser.add_argument('--dc', '--low_dim_cutoff', type=int, dest="low_dim_cutoff", default=None,
help='All bboxes with dims(w/h) < cutoff pixel are ignored i.e 400 (default: %(default)s)')
parser.add_argument('--ac', '--low_area_cutoff', type=float, dest="low_area_cutoff", default=None,
help='All bboxes with area perc < cutoff area perc are ignored i.e. 0.01 (default: %(default)s)')
args = parser.parse_args()
return args


def conv_visdrone_2_yolo(source_annot_dir: str, source_image_dir: str, target_annot_dir: str, low_dim_cutoff: Optional[int], low_area_cutoff: Optional[float]):
"""
low_dim_cutoff: int, lower cutoff for bounding boxes width/height dims in pixels
low_area_cutoff: float, lower area perc cutoff for bounding box areas in perc
"""
if not all([osp.isdir(source_annot_dir), osp.isdir(source_image_dir)]):
raise ValueError(f"source_annot_dir and source_image_dir must be directories")
src_annot_path = osp.join(source_annot_dir, "*")
src_image_path = osp.join(source_image_dir, "*")
src_annot_paths = sorted(glob.glob(src_annot_path))
src_image_paths = [p for p in sorted(glob.glob(src_image_path)) if osp.splitext(p)[-1] in IMG_EXT]
assert len(src_image_paths) == len(src_annot_paths)

os.makedirs(target_annot_dir, exist_ok=True)
low_dim_cutoff = float('-inf') if not low_dim_cutoff else low_dim_cutoff
low_area_cutoff = float('-inf') if not low_area_cutoff else low_area_cutoff
target_img_list_fpath = osp.join(osp.dirname(target_annot_dir), source_annot_dir.split('/')[-2].lower()+".txt")

with tqdm.tqdm(total=len(src_image_paths)) as pbar, open(target_img_list_fpath, "w") as imgw:
orig_box_count = new_box_count = 0
for src_annot_file, src_image_file in zip(src_annot_paths, src_image_paths):
try:
iw, ih = imagesize.get(src_image_file)
target_annot_file = osp.join(target_annot_dir, src_annot_file.split('/')[-1])
with open(src_annot_file, "r") as fr, open(target_annot_file, "w") as fw:
for coords in fr:
annots = list(map(int, coords.strip().strip(',').split(',')))
x, y = annots[0], annots[1]
w, h = annots[2], annots[3]
score, class_id, occu = annots[4], annots[5], annots[7]
if class_id not in CLASS_2_CONSIDER: # only keep classes to consider
continue
orig_box_count += 1
if w < low_dim_cutoff or h < low_dim_cutoff: # cutoff value for dims to remove outliers
continue
area_perc = 100 * (w * h) / (iw * ih)
if area_perc < low_area_cutoff: # cutoff value for area perc to remove outliers
continue

xc, yc = x + (w / 2), y + (h / 2)
# only use objects used for eval along and all levels of occlusion (0,1,2)
if score and occu <= 2:
class_id = CLASS_ID_REMAP[class_id] if CLASS_ID_REMAP else class_id
fw.write(f"{class_id} {xc/iw} {yc/ih} {w/iw} {h/ih}\n")
new_box_count += 1
target_image_path = osp.join(osp.dirname(osp.dirname(target_annot_file)), "images",
osp.basename(target_annot_file).split('.')[0] + osp.splitext(src_image_file)[1])
imgw.write(f"{osp.abspath(target_image_path)}\n")
except Exception as excep:
print(f"{excep}: Error reading img {src_image_file}")
pbar.update(1)
print(f"Original Box Count: {orig_box_count}. Converted Box Count {new_box_count}")
print(f"{100 * (new_box_count) / orig_box_count:.2f}% of total boxes kept")


def main():
args = get_parsed_args()
print(args)
conv_visdrone_2_yolo(args.source_annot_dir, args.source_image_dir, args.target_annot_dir,
args.low_dim_cutoff, args.low_area_cutoff)


if __name__ == "__main__":
main()
54 changes: 54 additions & 0 deletions adv_patch_gen/conv_visdrone_2_yolo/disp_visdrone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import argparse
import sys
sys.path.append(".")

import cv2
from adv_patch_gen.conv_visdrone_2_yolo.utils import get_annot_img_paths, load_visdrone_annots_as_np


# visdrone dataset classes
# ignore(0), pedestrian(1), people(2), bicycle(3),
# car(4), van(5), truck(6), tricycle(7), awning-tricycle(8),
# bus(9), motor(10), others(11)

ANNOT_EXT = {".txt"}
IMG_EXT = {".jpg", ".png"}
CLASS_IDS_2_CONSIDER = {4, 5, 6, 9}


def get_parsed_args():
parser = argparse.ArgumentParser(
description="Disp VisDrone annotated images")
parser.add_argument('-a', '--source_annot_dir', type=str, dest="source_annot_dir", required=True,
help='VisDrone annotation source dir. Should contain annot txt files (default: %(default)s)')
parser.add_argument('-i', '--source_image_dir', type=str, dest="source_image_dir", required=True,
help='VisDrone images source dir. Should contain image files (default: %(default)s)')
args = parser.parse_args()
return args


def main():
args = get_parsed_args()
annot_paths, image_paths = get_annot_img_paths(
args.source_annot_dir, args.source_image_dir, ANNOT_EXT, IMG_EXT)

for ant, img in zip(annot_paths, image_paths):
image = cv2.imread(img)
annots = load_visdrone_annots_as_np(ant)
for annot in annots:
score, class_id = annot[4], annot[5]
if class_id not in CLASS_IDS_2_CONSIDER: # car, van ,truck, bus
continue
color = (0, 0, 255) if score == 0 else (0, 255, 0)
x1, y1, x2, y2 = annot[:4]
cv2.rectangle(image, (x1, y1), (x2, y2), color, 1)

cv2.imshow("VisDrone annot visualized", image)
key = cv2.waitKey()
if key == ord("q"):
cv2.destroyAllWindows()
exit()


if __name__ == "__main__":
main()
59 changes: 59 additions & 0 deletions adv_patch_gen/conv_visdrone_2_yolo/disp_yolo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import argparse
import sys
sys.path.append(".")

import cv2
from adv_patch_gen.conv_visdrone_2_yolo.utils import get_annot_img_paths, load_yolo_annots_as_np


# visdrone dataset classes
# ignore(0), pedestrian(1), people(2), bicycle(3),
# car(4), van(5), truck(6), tricycle(7), awning-tricycle(8),
# bus(9), motor(10), others(11)
# CLASS_IDS_2_CONSIDER = {4, 5, 6, 9}

ANNOT_EXT = {".txt"}
IMG_EXT = {".jpg", ".png"}
# custom yolo classes
# car(0), van(1), truck(2), bus(3)
CLASS_IDS_2_CONSIDER = {0, 1, 2, 3}


def get_parsed_args():
parser = argparse.ArgumentParser(
description="Disp YOLO annotated images")
parser.add_argument('-a', '--source_annot_dir', type=str, dest="source_annot_dir", required=True,
help='YOLO annotation source dir. Should contain annot txt files (default: %(default)s)')
parser.add_argument('-i', '--source_image_dir', type=str, dest="source_image_dir", required=True,
help='YOLO images source dir. Should contain image files (default: %(default)s)')
args = parser.parse_args()
return args


def main():
args = get_parsed_args()
annot_paths, image_paths = get_annot_img_paths(
args.source_annot_dir, args.source_image_dir, ANNOT_EXT, IMG_EXT)

for ant, img in zip(annot_paths, image_paths):
image = cv2.imread(img)
annots = load_yolo_annots_as_np(ant)
for annot in annots:
class_id = annot[0]
if class_id not in CLASS_IDS_2_CONSIDER: # car, van ,truck, bus
continue
ih, iw, _ = image.shape
xc, yc, w, h = annot[1] * iw, annot[2] * ih, annot[3] * iw, annot[4] * ih
x1, y1, x2, y2 = xc - w / 2, yc - h / 2, xc + w / 2, yc + h / 2
x1, y1, x2, y2 = map(int, (x1, y1, x2, y2))
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 0, 255), 1)

cv2.imshow("YOLO annot visualized", image)
key = cv2.waitKey()
if key == ord("q"):
cv2.destroyAllWindows()
exit()


if __name__ == "__main__":
main()
39 changes: 39 additions & 0 deletions adv_patch_gen/conv_visdrone_2_yolo/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import glob
from typing import List, Tuple, Set

import numpy as np


def get_annot_img_paths(annot_dir: str, image_dir: str, annot_ext: Set[str], img_ext: Set[str]) -> Tuple[List[str], List[str]]:
annots_path = os.path.join(annot_dir, "*")
images_path = os.path.join(image_dir, "*")
annot_paths = [p for p in sorted(glob.glob(annots_path)) if os.path.splitext(p)[-1] in annot_ext]
image_paths = [p for p in sorted(glob.glob(images_path)) if os.path.splitext(p)[-1] in img_ext]

assert len(annot_paths) == len(image_paths)
return annot_paths, image_paths


def load_visdrone_annots_as_np(annot_file: str) -> np.ndarray:
annot_list = []
with open(annot_file, "r") as f:
for values in f:
annots = list(map(int, values.strip().strip(',').split(',')))
x1, y1 = annots[0], annots[1]
x2, y2 = x1 + annots[2], y1 + annots[3]
score, class_id, occu = annots[4], annots[5], annots[7]
annot_list.append([x1, y1, x2, y2, score, class_id, occu])
return np.asarray(annot_list)


def load_yolo_annots_as_np(annot_file: str) -> np.ndarray:
annot_list = []
with open(annot_file, "r") as f:
for values in f:
annots = list(map(float, values.strip().split()))
class_id = annots[0]
xc, yc = annots[1], annots[2]
w, h = annots[3], annots[4]
annot_list.append([class_id, xc, yc, w, h])
return np.asarray(annot_list)
30 changes: 30 additions & 0 deletions adv_patch_gen/test_notebooks/30_rgb_triplets.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
0.10588,0.054902,0.1098
0.48235,0.094118,0.16863
0.50196,0.52549,0.17647
0.082353,0.31765,0.18431
0.47843,0.61176,0.51765
0.07451,0.3098,0.45882
0.67843,0.14902,0.18039
0.086275,0.14118,0.26275
0.26667,0.36863,0.47843
0.76078,0.54118,0.5451
0.73333,0.49412,0.27451
0.25882,0.35294,0.18039
0.47843,0.22353,0.36471
0.27059,0.086275,0.11765
0.7098,0.32157,0.2
0.27451,0.13725,0.29412
0.75294,0.75686,0.63137
0.28627,0.54902,0.41176
0.47451,0.2902,0.15294
0.74902,0.70196,0.28627
0.098039,0.42745,0.44314
0.50588,0.65098,0.65882
0.12549,0.42745,0.23529
0.4902,0.58431,0.33725
0.26275,0.49412,0.26275
0.07451,0.14902,0.12549
0.090196,0.20392,0.36078
0.68627,0.15686,0.30196
0.30196,0.5451,0.57647
0.71765,0.32941,0.40784
Loading

0 comments on commit 94be9ce

Please sign in to comment.