Module brevettiai.data.image.annotation_parser
Expand source code
import json
import logging
import numpy as np
import pandas as pd
from brevettiai.io import io_tools
from brevettiai.data.image import ImageKeys
from functools import partial
import uuid
log = logging.getLogger(__name__)
try:
import cv2
except ImportError as e:
log.warning("CV2 not available")
def get_points(points, offset=np.array((0, 0)), scale=1):
"""
Get points from annotation
Offset is given in original coordinates, and is applied before scaling
:param points:
:param offset:
:param scale:
:return:
"""
p = np.fromiter((y for x in points for y in (x["x"], x["y"])),
dtype=np.float, count=len(points)*2)
p.shape = len(points), 2
p = (p+offset[None])*scale
return p
def set_points(points):
pt_list = [None] * len(points)
for ii, pt in enumerate(points):
pt_list[ii] = dict(x=pt[0, 0], y=pt[0, 1])
return pt_list
def get_bbox(annotation):
"""
Get bounding box bbox from an annotation
:param annotation:
:return:
"""
try:
if annotation["type"] in {"rectangle", "polygon", "point", "line"}: # and "roi" in ann["label"]:
p = get_points(annotation["points"]).astype(np.int)
return np.concatenate((p.min(axis=0), p.max(axis=0))).clip(min=0), p
except Exception as ex:
log.debug("Error getting bounding box bbox of annotation", exc_info=ex)
return None, None
def sample_points_in_annotation(annotation, tries=100000):
cnt = make_contour(annotation["points"], annotation["type"])[0]
if len(cnt) <= 2:
for i in np.random.choice(np.arange(len(cnt)), size=1000):
yield cnt[i]
else:
_min, _max = cnt.min(axis=0), cnt.max(axis=0)
_range = (_max - _min)
for i in range(tries):
p = (np.random.rand(2) * _range) + _min
if cv2.pointPolygonTest(cnt, tuple(p), False) >= 0:
yield np.round(p).astype(np.int)
def get_image_info(annotation_path):
annotation = json.loads(io_tools.read_file(annotation_path).decode())
return annotation["image"]
def bounding_box_area(bbox):
if bbox is not None:
return (bbox[2]-bbox[0]) * (bbox[3]-bbox[1])
else:
return -1
def get_annotations(segmentation_path, io=io_tools):
if len(segmentation_path) > 0:
ann_file = json.loads(io.read_file(segmentation_path))
annotations = ann_file.get("annotations", [])
for a in annotations:
a[ImageKeys.BOUNDING_BOX], points = get_bbox(a)
if bounding_box_area(a[ImageKeys.BOUNDING_BOX]) > 0 and a["type"] in {"rectangle", "polygon"}:
try:
a[ImageKeys.INSIDE_POINTS] = np.stack(p for i, p in zip(range(10), sample_points_in_annotation(a)))
except ValueError:
a[ImageKeys.INSIDE_POINTS] = np.stack(points[i % len(points)] for i in range(10))
elif points is not None:
a[ImageKeys.INSIDE_POINTS] = np.stack(points[i % len(points)] for i in range(10))
if "uuid" not in a:
a["uuid"] = "Missing - " + str(uuid.uuid4())
for k in ("color", "points"):
a.pop(k, None)
return annotations
else:
return []
def expand_samples_with_annotations(samples, verbose=1, key="segmentation_path", how="outer", io=io_tools):
"""
Expand samples DataFrame such that each annotation results in a new sample
:param samples: Pandas dataframe with
:param verbose:
:param key: Key in samples with segmentation path
:return:
"""
assert samples.index.is_unique
if verbose > 0:
from tqdm import tqdm
tqdm.pandas()
apply = samples[key].progress_apply
else:
apply = samples[key].apply
ann = apply(partial(get_annotations, io=io))
ann = ann[ann.str.len() > 0]
exploded = ann.explode()
meta = pd.DataFrame(exploded.tolist(), index=exploded.index)
if "visibility" not in meta.columns:
meta["visibility"] = -1
if "severity" not in meta.columns:
meta["severity"] = -1
out = pd.merge(samples, meta, left_index=True, right_index=True, how=how)
out["visibility"].fillna(-1, inplace=True)
out["severity"].fillna(-1, inplace=True)
return out
def map_segmentations(annotations, segmentation_mapping):
for seg in annotations:
if segmentation_mapping:
label = segmentation_mapping.get(seg["label"], seg["label"])
seg["label"] = (label,) if isinstance(label, str) else tuple(label)
else:
seg["label"] = (seg["label"],)
return True
def make_contour(points, anno_type, point_size=1, offset=np.array((0, 0)), scale=1):
def circle_points(center, r):
alphas = np.linspace(0, 2 * np.pi, int(np.ceil(2 * np.pi * r)))
return np.array([center[0] + r * np.cos(alphas), center[1] + r * np.sin(alphas)]).T
if anno_type == "polygon":
pt_list = get_points(points, offset, scale)
elif anno_type == "rectangle":
pt_list = get_points(points, offset, scale)
pt_list = np.array([pt_list[[0, 0, 1, 1], 0], pt_list[[0, 1, 1, 0], 1]]).T
elif anno_type == "point":
pt_list = get_points(points, offset, scale).astype(np.int)
if point_size > 1:
r = int(np.ceil(point_size / 2.0))
pt_list = circle_points(pt_list[0, 0], r)
elif anno_type == "line":
pt_list = get_points(points, offset, scale)
pt_list = np.floor(pt_list).astype(np.int)
return [pt_list[ii:ii+2] for ii in range(len(pt_list)-1)]
else:
return None
# drawContours works with pixel centers, but annotation tool use upper left corner as reference
return [np.floor(pt_list).astype(np.int)]
def draw_contours2(segmentation, label_space, bbox=None, draw_buffer=None, drawContoursArgs=None, **kwargs):
"""
If more than four channels are in the label space only values 1 will be drawn to the segmentation
:param segmentation:
:param label_space:
:param bbox: bbox of annotation to generate [x0,y0,x1,y1]
:param draw_buffer: input draw buffer, use to draw on top of existing images
:param drawContoursArgs: Args for drawContours.. eg thickness to draw non filled contours
:param kwargs: args for make_contours
:return:
"""
if drawContoursArgs is None:
drawContoursArgs = dict(thickness=cv2.FILLED)
if draw_buffer is None:
# Apply bbox to shape for buffer
if bbox is None:
shape = (segmentation["image"]["height"], segmentation["image"]["width"])
else:
shape = (bbox[3] - bbox[1], bbox[2] - bbox[0])
first_label = next(iter(label_space.values()))
shape = (*shape, len(first_label))
cont = np.zeros(shape, dtype=np.float32)
else:
cont = draw_buffer
if bbox is not None:
kwargs["offset"] = -bbox[:2]
for lbl, color in label_space.items():
color = color if isinstance(color, (tuple, list)) else color.tolist()
contours = []
for anno in segmentation["annotations"]:
if lbl == anno["label"] or (isinstance(lbl, tuple) and np.any([lbl_ii == anno["label"] for lbl_ii in lbl])):
contour = make_contour(anno["points"], anno["type"], **kwargs)
if contour is not None:
contours.extend(contour)
# If any contours are found, draw non zero label items
if len(contours):
for i, c in enumerate(color):
if c != 0:
cont[..., i] = cv2.drawContours(cont[..., i].copy(), contours, -1, c, **drawContoursArgs)
return cont
def draw_contours2_CHW(segmentation, label_space, bbox=None, draw_buffer=None, drawContoursArgs=None, **kwargs):
"""
If more than four channels are in the label space only values 1 will be drawn to the segmentation
:param segmentation:
:param label_space:
:param bbox: bbox of annotation to generate [x0,y0,x1,y1]
:param draw_buffer: input draw buffer, use to draw on top of existing images
:param drawContoursArgs: Args for drawContours.. eg thickness to draw non filled contours
:param kwargs: args for make_contours
:return:
"""
if drawContoursArgs is None:
drawContoursArgs = dict(thickness=cv2.FILLED)
if draw_buffer is None:
# Apply bbox to shape for buffer
if bbox is None:
shape = (segmentation["image"]["height"], segmentation["image"]["width"])
else:
shape = (bbox[3] - bbox[1], bbox[2] - bbox[0])
first_label = next(iter(label_space.values()))
shape = (len(first_label), *shape)
cont = np.zeros(shape, dtype=np.float32)
else:
cont = draw_buffer
if bbox is not None:
kwargs["offset"] = -bbox[:2]
for lbl, color in label_space.items():
contours = []
for anno in segmentation["annotations"]:
if lbl == anno["label"] or (isinstance(lbl, tuple) and np.any([lbl_ii == anno["label"] for lbl_ii in lbl])):
contour = make_contour(anno["points"], anno["type"], **kwargs)
if contour is not None:
contours.extend(contour)
# If any contours are found, draw non zero label items
if len(contours):
color = color if isinstance(color, (tuple, list)) else color.tolist()
for i, c in enumerate(color):
if c != 0:
cv2.drawContours(cont[i], contours, -1, c, **drawContoursArgs)
return cont
Functions
def bounding_box_area(bbox)
-
Expand source code
def bounding_box_area(bbox): if bbox is not None: return (bbox[2]-bbox[0]) * (bbox[3]-bbox[1]) else: return -1
def draw_contours2(segmentation, label_space, bbox=None, draw_buffer=None, drawContoursArgs=None, **kwargs)
-
If more than four channels are in the label space only values 1 will be drawn to the segmentation :param segmentation: :param label_space: :param bbox: bbox of annotation to generate [x0,y0,x1,y1] :param draw_buffer: input draw buffer, use to draw on top of existing images :param drawContoursArgs: Args for drawContours.. eg thickness to draw non filled contours :param kwargs: args for make_contours :return:
Expand source code
def draw_contours2(segmentation, label_space, bbox=None, draw_buffer=None, drawContoursArgs=None, **kwargs): """ If more than four channels are in the label space only values 1 will be drawn to the segmentation :param segmentation: :param label_space: :param bbox: bbox of annotation to generate [x0,y0,x1,y1] :param draw_buffer: input draw buffer, use to draw on top of existing images :param drawContoursArgs: Args for drawContours.. eg thickness to draw non filled contours :param kwargs: args for make_contours :return: """ if drawContoursArgs is None: drawContoursArgs = dict(thickness=cv2.FILLED) if draw_buffer is None: # Apply bbox to shape for buffer if bbox is None: shape = (segmentation["image"]["height"], segmentation["image"]["width"]) else: shape = (bbox[3] - bbox[1], bbox[2] - bbox[0]) first_label = next(iter(label_space.values())) shape = (*shape, len(first_label)) cont = np.zeros(shape, dtype=np.float32) else: cont = draw_buffer if bbox is not None: kwargs["offset"] = -bbox[:2] for lbl, color in label_space.items(): color = color if isinstance(color, (tuple, list)) else color.tolist() contours = [] for anno in segmentation["annotations"]: if lbl == anno["label"] or (isinstance(lbl, tuple) and np.any([lbl_ii == anno["label"] for lbl_ii in lbl])): contour = make_contour(anno["points"], anno["type"], **kwargs) if contour is not None: contours.extend(contour) # If any contours are found, draw non zero label items if len(contours): for i, c in enumerate(color): if c != 0: cont[..., i] = cv2.drawContours(cont[..., i].copy(), contours, -1, c, **drawContoursArgs) return cont
def draw_contours2_CHW(segmentation, label_space, bbox=None, draw_buffer=None, drawContoursArgs=None, **kwargs)
-
If more than four channels are in the label space only values 1 will be drawn to the segmentation :param segmentation: :param label_space: :param bbox: bbox of annotation to generate [x0,y0,x1,y1] :param draw_buffer: input draw buffer, use to draw on top of existing images :param drawContoursArgs: Args for drawContours.. eg thickness to draw non filled contours :param kwargs: args for make_contours :return:
Expand source code
def draw_contours2_CHW(segmentation, label_space, bbox=None, draw_buffer=None, drawContoursArgs=None, **kwargs): """ If more than four channels are in the label space only values 1 will be drawn to the segmentation :param segmentation: :param label_space: :param bbox: bbox of annotation to generate [x0,y0,x1,y1] :param draw_buffer: input draw buffer, use to draw on top of existing images :param drawContoursArgs: Args for drawContours.. eg thickness to draw non filled contours :param kwargs: args for make_contours :return: """ if drawContoursArgs is None: drawContoursArgs = dict(thickness=cv2.FILLED) if draw_buffer is None: # Apply bbox to shape for buffer if bbox is None: shape = (segmentation["image"]["height"], segmentation["image"]["width"]) else: shape = (bbox[3] - bbox[1], bbox[2] - bbox[0]) first_label = next(iter(label_space.values())) shape = (len(first_label), *shape) cont = np.zeros(shape, dtype=np.float32) else: cont = draw_buffer if bbox is not None: kwargs["offset"] = -bbox[:2] for lbl, color in label_space.items(): contours = [] for anno in segmentation["annotations"]: if lbl == anno["label"] or (isinstance(lbl, tuple) and np.any([lbl_ii == anno["label"] for lbl_ii in lbl])): contour = make_contour(anno["points"], anno["type"], **kwargs) if contour is not None: contours.extend(contour) # If any contours are found, draw non zero label items if len(contours): color = color if isinstance(color, (tuple, list)) else color.tolist() for i, c in enumerate(color): if c != 0: cv2.drawContours(cont[i], contours, -1, c, **drawContoursArgs) return cont
def expand_samples_with_annotations(samples, verbose=1, key='segmentation_path', how='outer', io=<brevettiai.io.utils.IoTools object>)
-
Expand samples DataFrame such that each annotation results in a new sample :param samples: Pandas dataframe with :param verbose: :param key: Key in samples with segmentation path :return:
Expand source code
def expand_samples_with_annotations(samples, verbose=1, key="segmentation_path", how="outer", io=io_tools): """ Expand samples DataFrame such that each annotation results in a new sample :param samples: Pandas dataframe with :param verbose: :param key: Key in samples with segmentation path :return: """ assert samples.index.is_unique if verbose > 0: from tqdm import tqdm tqdm.pandas() apply = samples[key].progress_apply else: apply = samples[key].apply ann = apply(partial(get_annotations, io=io)) ann = ann[ann.str.len() > 0] exploded = ann.explode() meta = pd.DataFrame(exploded.tolist(), index=exploded.index) if "visibility" not in meta.columns: meta["visibility"] = -1 if "severity" not in meta.columns: meta["severity"] = -1 out = pd.merge(samples, meta, left_index=True, right_index=True, how=how) out["visibility"].fillna(-1, inplace=True) out["severity"].fillna(-1, inplace=True) return out
def get_annotations(segmentation_path, io=<brevettiai.io.utils.IoTools object>)
-
Expand source code
def get_annotations(segmentation_path, io=io_tools): if len(segmentation_path) > 0: ann_file = json.loads(io.read_file(segmentation_path)) annotations = ann_file.get("annotations", []) for a in annotations: a[ImageKeys.BOUNDING_BOX], points = get_bbox(a) if bounding_box_area(a[ImageKeys.BOUNDING_BOX]) > 0 and a["type"] in {"rectangle", "polygon"}: try: a[ImageKeys.INSIDE_POINTS] = np.stack(p for i, p in zip(range(10), sample_points_in_annotation(a))) except ValueError: a[ImageKeys.INSIDE_POINTS] = np.stack(points[i % len(points)] for i in range(10)) elif points is not None: a[ImageKeys.INSIDE_POINTS] = np.stack(points[i % len(points)] for i in range(10)) if "uuid" not in a: a["uuid"] = "Missing - " + str(uuid.uuid4()) for k in ("color", "points"): a.pop(k, None) return annotations else: return []
def get_bbox(annotation)
-
Get bounding box bbox from an annotation :param annotation: :return:
Expand source code
def get_bbox(annotation): """ Get bounding box bbox from an annotation :param annotation: :return: """ try: if annotation["type"] in {"rectangle", "polygon", "point", "line"}: # and "roi" in ann["label"]: p = get_points(annotation["points"]).astype(np.int) return np.concatenate((p.min(axis=0), p.max(axis=0))).clip(min=0), p except Exception as ex: log.debug("Error getting bounding box bbox of annotation", exc_info=ex) return None, None
def get_image_info(annotation_path)
-
Expand source code
def get_image_info(annotation_path): annotation = json.loads(io_tools.read_file(annotation_path).decode()) return annotation["image"]
def get_points(points, offset=array([0, 0]), scale=1)
-
Get points from annotation Offset is given in original coordinates, and is applied before scaling :param points: :param offset: :param scale: :return:
Expand source code
def get_points(points, offset=np.array((0, 0)), scale=1): """ Get points from annotation Offset is given in original coordinates, and is applied before scaling :param points: :param offset: :param scale: :return: """ p = np.fromiter((y for x in points for y in (x["x"], x["y"])), dtype=np.float, count=len(points)*2) p.shape = len(points), 2 p = (p+offset[None])*scale return p
def make_contour(points, anno_type, point_size=1, offset=array([0, 0]), scale=1)
-
Expand source code
def make_contour(points, anno_type, point_size=1, offset=np.array((0, 0)), scale=1): def circle_points(center, r): alphas = np.linspace(0, 2 * np.pi, int(np.ceil(2 * np.pi * r))) return np.array([center[0] + r * np.cos(alphas), center[1] + r * np.sin(alphas)]).T if anno_type == "polygon": pt_list = get_points(points, offset, scale) elif anno_type == "rectangle": pt_list = get_points(points, offset, scale) pt_list = np.array([pt_list[[0, 0, 1, 1], 0], pt_list[[0, 1, 1, 0], 1]]).T elif anno_type == "point": pt_list = get_points(points, offset, scale).astype(np.int) if point_size > 1: r = int(np.ceil(point_size / 2.0)) pt_list = circle_points(pt_list[0, 0], r) elif anno_type == "line": pt_list = get_points(points, offset, scale) pt_list = np.floor(pt_list).astype(np.int) return [pt_list[ii:ii+2] for ii in range(len(pt_list)-1)] else: return None # drawContours works with pixel centers, but annotation tool use upper left corner as reference return [np.floor(pt_list).astype(np.int)]
def map_segmentations(annotations, segmentation_mapping)
-
Expand source code
def map_segmentations(annotations, segmentation_mapping): for seg in annotations: if segmentation_mapping: label = segmentation_mapping.get(seg["label"], seg["label"]) seg["label"] = (label,) if isinstance(label, str) else tuple(label) else: seg["label"] = (seg["label"],) return True
def sample_points_in_annotation(annotation, tries=100000)
-
Expand source code
def sample_points_in_annotation(annotation, tries=100000): cnt = make_contour(annotation["points"], annotation["type"])[0] if len(cnt) <= 2: for i in np.random.choice(np.arange(len(cnt)), size=1000): yield cnt[i] else: _min, _max = cnt.min(axis=0), cnt.max(axis=0) _range = (_max - _min) for i in range(tries): p = (np.random.rand(2) * _range) + _min if cv2.pointPolygonTest(cnt, tuple(p), False) >= 0: yield np.round(p).astype(np.int)
def set_points(points)
-
Expand source code
def set_points(points): pt_list = [None] * len(points) for ii, pt in enumerate(points): pt_list[ii] = dict(x=pt[0, 0], y=pt[0, 1]) return pt_list