def __init__(self, width, height, min_combined_ratio, min_temporal_IOU, max_gap, verbose=False): self.width = width self.height = height self.min_combined_ratio = min_combined_ratio self.min_temporal_IOU = min_temporal_IOU self.max_gap = max_gap self.unique_bbox_objects = [] self.unique_bbox_frames = [] self.bbox_idx_per_frame = [] self.bbox_int_index_x = IntervalIndex(True) self.bbox_int_index_y = IntervalIndex(True) self.img_idx = 0 self.tempo_count = 0 # optimizing the matching ... self.bbox_last_frame = [] self.bbox_active = [] self.verbose = verbose
def __init__(self, width, height, min_recall, min_precision, max_gap, verbose=False): self.width = width self.height = height self.min_recall = min_recall self.min_precision = min_precision self.max_gap = max_gap self.unique_cc_objects = [] self.unique_cc_frames = [] self.cc_idx_per_frame = [] self.cc_int_index_x = IntervalIndex(True) self.cc_int_index_y = IntervalIndex(True) self.fake_age = np.zeros((height, width), dtype=np.float32) self.img_idx = 0 self.tempo_count = 0 # optimizing the matching ... self.cc_last_frame = [] self.cc_active = [] self.verbose = verbose
class BBoxStabilityEstimator: RefinePerBoxPerSegment = 1 RefineAllBoxesPerSegment = 2 RefineAllBoxesAllSegments = 3 def __init__(self, width, height, min_combined_ratio, min_temporal_IOU, max_gap, verbose=False): self.width = width self.height = height self.min_combined_ratio = min_combined_ratio self.min_temporal_IOU = min_temporal_IOU self.max_gap = max_gap self.unique_bbox_objects = [] self.unique_bbox_frames = [] self.bbox_idx_per_frame = [] self.bbox_int_index_x = IntervalIndex(True) self.bbox_int_index_y = IntervalIndex(True) self.img_idx = 0 self.tempo_count = 0 # optimizing the matching ... self.bbox_last_frame = [] self.bbox_active = [] self.verbose = verbose def get_raw_bbox_count(self): total = 0 for current_frame in self.bbox_idx_per_frame: total += len(current_frame) return total def get_inter_bbox(self, box1, box2): b1_x1, b1_y1, b1_x2, b1_y2 = box1 b2_x1, b2_y1, b2_x2, b2_y2 = box2 comb_inter_x1 = max(b1_x1, b2_x1) comb_inter_y1 = max(b1_y1, b2_y1) comb_inter_x2 = min(b1_x2, b2_x2) comb_inter_y2 = min(b1_y2, b2_y2) return comb_inter_x1, comb_inter_y1, comb_inter_x2, comb_inter_y2 def get_outer_bbox(self, box1, box2): b1_x1, b1_y1, b1_x2, b1_y2 = box1 b2_x1, b2_y1, b2_x2, b2_y2 = box2 comb_outer_x1 = min(b1_x1, b2_x1) comb_outer_y1 = min(b1_y1, b2_y1) comb_outer_x2 = max(b1_x2, b2_x2) comb_outer_y2 = max(b1_y2, b2_y2) return comb_outer_x1, comb_outer_y1, comb_outer_x2, comb_outer_y2 def get_bbox_area(self, box): x1, y1, x2, y2 = box b1_w = x2 - x1 b1_h = y2 - y1 # check for invalid boxes with negative width or height if b1_w <= 0.0 or b1_h <= 0.0: return 0.0 return b1_w * b1_h def get_bboxes_IOU(self, box1, box2): # area of first box ... b1_area = self.get_bbox_area(box1) # area of second box ... b2_area = self.get_bbox_area(box2) # intersection between two boxes ... comb_inter = self.get_inter_bbox(box1, box2) inter_area = self.get_bbox_area(comb_inter) combined_union_area = b1_area + b2_area - inter_area # print((b1_area, b2_area, box1, box2, comb_inter, inter_area, combined_union_area)) return inter_area / combined_union_area def get_combined_box_area_ratio(self, box1, box2): # area of first box ... b1_area = self.get_bbox_area(box1) # area of second box ... b2_area = self.get_bbox_area(box2) # intersection between two boxes ... comb_inter = self.get_inter_bbox(box1, box2) inter_area = self.get_bbox_area(comb_inter) combined_union_area = b1_area + b2_area - inter_area comb_outer = self.get_outer_bbox(box1, box2) outer_area = self.get_bbox_area(comb_outer) area_ratio = combined_union_area / outer_area return area_ratio def visualize_boxes(self, bboxes, color=(255, 0, 0)): out_img = np.zeros((self.height, self.width, 3), np.uint8) for x1, y1, x2, y2 in bboxes: cv2.rectangle(out_img, (int(x1), int(y1)), (int(x2), int(y2)), color, thickness=2) return out_img def spatial_box_grouping(self, frame_bboxes, min_combined_ratio=None): if min_combined_ratio is None: min_combined_ratio = self.min_combined_ratio current_boxes = list(frame_bboxes) merged_boxes = True while merged_boxes: merged_boxes = False # sort bounding boxes by descending size ... boxes_by_size = [] for x1, y1, x2, y2 in current_boxes: w = x2 - x1 + 1 h = y2 - y1 + 1 area = w * h boxes_by_size.append((area, (x1, y1, x2, y2))) boxes_by_size = sorted(boxes_by_size, reverse=True, key=lambda x: x[0]) # print(boxes_by_size) # create interval index to find matches quicker (all boxes against all boxes from same frame) int_index_x = IntervalIndex(True) int_index_y = IntervalIndex(True) for box_idx, (area, (x1, y1, x2, y2)) in enumerate(boxes_by_size): int_index_x.add(x1, x2 + 1, box_idx) int_index_y.add(y1, y2 + 1, box_idx) # ... find pair-wise matches ... set_x = set(int_index_x.find_matches(int_index_x)) set_y = set(int_index_y.find_matches(int_index_y)) # .... list of all pairs of boxes with intersecting intervals in X and Y merge_candidates = sorted(list(set_x.intersection(set_y))) # ... filter self-matches and repetitions ... merge_candidates = [(box_idx1, box_idx2) for box_idx1, box_idx2 in merge_candidates if box_idx1 < box_idx2] # ... split by first box .. candidates_by_box = {idx: [] for idx in range(len(boxes_by_size))} for box_idx1, box_idx2 in merge_candidates: candidates_by_box[box_idx1].append(box_idx2) # print(merge_candidates) # print(candidates_by_box) box_added = [False] * len(boxes_by_size) current_boxes = [] # for each box (sorted by size) for box_idx in range(len(boxes_by_size)): # if this box has been previously added (merged with ealier box) if box_added[box_idx]: # skip ... continue box_added[box_idx] = True # current box boundaries c_area, (c_x1, c_y1, c_x2, c_y2) = boxes_by_size[box_idx] for second_box_idx in candidates_by_box[box_idx]: if box_added[second_box_idx]: # skip merge candidate ... continue # get box boundaries ... o_area, (o_x1, o_y1, o_x2, o_y2) = boxes_by_size[second_box_idx] comb_area_ratio = self.get_combined_box_area_ratio( (c_x1, c_y1, c_x2, c_y2), (o_x1, o_y1, o_x2, o_y2)) # print(((c_x1, c_y1, c_x2, c_y2), boxes_by_size[second_box_idx], comb_area_ratio, box_idx, second_box_idx)) if comb_area_ratio >= min_combined_ratio: # merge! # expand current bounding box to include the smaller box ... c_x1 = min(c_x1, o_x1) c_y1 = min(c_y1, o_y1) c_x2 = max(c_x2, o_x2) c_y2 = max(c_y2, o_y2) # mark second box as added, so it won't be added to the current list .. box_added[second_box_idx] = True merged_boxes = True # add to the next set of accepted boxes current_boxes.append((c_x1, c_y1, c_x2, c_y2)) """ if len(frame_bboxes) > 30: original_img = self.visualize_boxes(frame_bboxes, (255, 0, 0)) final_img = self.visualize_boxes(current_boxes, (0, 255, 0)) final_img[:, :, 0] = original_img[:, :, 0] debug_img = cv2.resize(final_img,(960, 540)) cv2.imshow("check", debug_img) cv2.waitKey() raise Exception("Error!") """ return current_boxes def add_frame(self, frame_bboxes): current_bboxes = self.spatial_box_grouping(frame_bboxes, self.min_combined_ratio) current_bboxes_idxs = [] if self.img_idx == 0: # simply copy all for bbox_id, bbox in enumerate(current_bboxes): # add the box to list of unique boxes ... self.unique_bbox_objects.append(bbox) # frames on which the bbox appears, raw label assigned to that CC self.unique_bbox_frames.append([(0, bbox_id)]) bbox_idx = len(self.unique_bbox_objects) - 1 current_bboxes_idxs.append((bbox_idx, bbox)) self.bbox_last_frame.append(0) self.bbox_active.append(bbox_idx) # add to indices ... x1, y1, x2, y2 = bbox self.bbox_int_index_x.add(x1, x2, bbox_idx) self.bbox_int_index_y.add(y1, y2, bbox_idx) else: # create indices for current bboxes other_index_x = IntervalIndex(True) other_index_y = IntervalIndex(True) for bbox_idx, (x1, y1, x2, y2) in enumerate(current_bboxes): other_index_x.add(x1, x2, bbox_idx) other_index_y.add(y1, y2, bbox_idx) # compute CC with matching regions set_x = set(other_index_x.find_matches(self.bbox_int_index_x)) set_y = set(other_index_y.find_matches(self.bbox_int_index_y)) # list of all pairs of CC with intersecting intervals in X and Y merged = sorted(list(set_x.intersection(set_y))) self.tempo_count += len(merged) # check every matching CC pre_add_size = len(self.bbox_active) next_match_idx = 0 for bbox_idx, bbox in enumerate(current_bboxes): found = False # check all matches in the list of matches for current CC while next_match_idx < len( merged) and merged[next_match_idx][0] == bbox_idx: if not found: prev_idx = merged[next_match_idx][1] prev_bbox = self.unique_bbox_objects[prev_idx] bbox_IOU = self.get_bboxes_IOU(bbox, prev_bbox) # print(bbox_IOU) if bbox_IOU >= self.min_temporal_IOU: # assume they are equivalent found = True self.unique_bbox_frames[prev_idx].append( (self.img_idx, bbox_idx)) current_bboxes_idxs.append((prev_idx, bbox)) # update last frame seen for this cc... self.bbox_last_frame[prev_idx] = self.img_idx next_match_idx += 1 # Not match was found? if not found: # add self.unique_bbox_objects.append(bbox) self.unique_bbox_frames.append([(self.img_idx, bbox_idx)]) new_bbox_idx = len(self.unique_bbox_objects) - 1 current_bboxes_idxs.append((new_bbox_idx, bbox)) self.bbox_last_frame.append(self.img_idx) self.bbox_active.append(new_bbox_idx) # add to indices ... x1, y1, x2, y2 = bbox self.bbox_int_index_x.add(x1, x2, new_bbox_idx) self.bbox_int_index_y.add(y1, y2, new_bbox_idx) # remove CC that are no longer active pre_remove_size = len(self.bbox_active) tempo_pos = 0 while tempo_pos < len(self.bbox_active): bbox_idx = self.bbox_active[tempo_pos] if self.img_idx - self.bbox_last_frame[ bbox_idx] >= self.max_gap: # no longer active .. # delete from active list del self.bbox_active[tempo_pos] # delete from interval indices bbox = self.unique_bbox_objects[bbox_idx] x1, y1, x2, y2 = bbox self.bbox_int_index_x.remove(x1, x2, bbox_idx) self.bbox_int_index_y.remove(y1, y2, bbox_idx) #print self.cc_last_frame[cc_idx], else: # still active tempo_pos += 1 """ total_added = pre_remove_size - pre_add_size total_removed = pre_remove_size - len(self.bbox_active) msg = "{0:d} , (Added: {1:d}, Removed: {2:d})".format(len(self.bbox_active), total_added, total_removed) print(msg) """ self.bbox_idx_per_frame.append(current_bboxes_idxs) self.img_idx += 1 if self.verbose: msg = "[{0:d} ({1:d}, {2:d})]".format( self.img_idx, len(current_bboxes), len(self.unique_bbox_objects)) print(msg, end="\r") def finish_processing(self): if self.verbose: print(".") def split_stable_bboxes_by_gaps(self, max_gap, stable_min_frames): splitted_count = 0 n_original_objects = len(self.unique_bbox_objects) for bbox_idx in range(n_original_objects): current_frames = self.unique_bbox_frames[bbox_idx] n_local = len(current_frames) current_group = [current_frames[0]] valid_groups = [current_group] for frame_offset in range(1, n_local): curr_frame_idx = current_frames[frame_offset][0] prev_frame_idx = current_frames[frame_offset - 1][0] current_gap = curr_frame_idx - prev_frame_idx if current_gap > max_gap: # not acceptable gap .. current_group = [current_frames[frame_offset]] valid_groups.append(current_group) else: # acceptable current_group.append(current_frames[frame_offset]) if len(valid_groups) >= 2 and n_local >= stable_min_frames: # replace the frames in the original cc list to only the first group .. self.unique_bbox_frames[bbox_idx] = valid_groups[0] # for each group (new CC) for group_offset in range(1, len(valid_groups)): new_bbox_idx = len(self.unique_bbox_objects) # add another reference to the original CC self.unique_bbox_objects.append( self.unique_bbox_objects[bbox_idx]) # add CC frame reference ... self.unique_bbox_frames.append(valid_groups[group_offset]) # for each frame where the orginal appeared .. for frame_idx, local_bbox_idx in valid_groups[ group_offset]: # find the CC on the frame ... for offset, (global_bbox_idx, local_bbox) in enumerate( self.bbox_idx_per_frame[frame_idx]): if global_bbox_idx == bbox_idx: # replace self.bbox_idx_per_frame[frame_idx][offset] = ( new_bbox_idx, local_bbox) break splitted_count += 1 return splitted_count def get_stable_bbox_idxs(self, min_stable_frames): stable_idxs = [] for bbox_idx in range(len(self.unique_bbox_objects)): if len(self.unique_bbox_frames[bbox_idx]) >= min_stable_frames: stable_idxs.append(bbox_idx) return stable_idxs def compute_overlapping_stable_bboxes(self, stable_idxs, temporal_window): n_objects = len(self.unique_bbox_objects) n_stable = len(stable_idxs) all_overlapping_cc = [[] for x in range(n_objects)] time_overlapping_cc = [[] for x in range(n_objects)] total_intersections = 0 for offset1 in range(n_stable): # get first stable bbox_idx_1 = stable_idxs[offset1] bbox_1 = self.unique_bbox_objects[bbox_idx_1] bbox_1_t_start = self.unique_bbox_frames[bbox_idx_1][0][0] bbox_1_t_end = self.unique_bbox_frames[bbox_idx_1][-1][0] bbox_1_area = self.get_bbox_area(bbox_1) print("Processing: " + str(offset1), end="\r") for offset2 in range(offset1 + 1, n_stable): bbox_idx_2 = stable_idxs[offset2] bbox_2 = self.unique_bbox_objects[bbox_idx_2] bbox_2_t_start = self.unique_bbox_frames[bbox_idx_2][0][0] bbox_2_t_end = self.unique_bbox_frames[bbox_idx_2][-1][0] # check intersection in space IOU = self.get_bboxes_IOU(bbox_1, bbox_2) if IOU > 0.0001: bbox_2_area = self.get_bbox_area(bbox_2) inter_box_area = self.get_bbox_area( self.get_inter_bbox(bbox_1, bbox_2)) all_overlapping_cc[bbox_idx_1].append( (bbox_idx_2, inter_box_area, bbox_2_area, bbox_1_area)) all_overlapping_cc[bbox_idx_2].append( (bbox_idx_1, inter_box_area, bbox_1_area, bbox_2_area)) # now, check intersection in time (considering time window) if ((bbox_1_t_end + temporal_window >= bbox_2_t_start) and (bbox_2_t_end >= bbox_1_t_start - temporal_window)): ratio_1 = inter_box_area / bbox_1_area ratio_2 = inter_box_area / bbox_2_area # they have some intersection in pixels.... time_overlapping_cc[bbox_idx_1].append( (bbox_idx_2, ratio_1, ratio_2)) time_overlapping_cc[bbox_idx_2].append( (bbox_idx_1, ratio_2, ratio_1)) total_intersections += 1 return time_overlapping_cc, total_intersections, all_overlapping_cc def compute_groups(self, stable_idxs, overlapping_bboxes): n_stable = len(stable_idxs) # Determine Groups of CC's that coexist in time and space and treat them as single units... bboxes_groups = [] group_idx_per_bbox = {} for offset1 in range(n_stable): bbox_idx_1 = stable_idxs[offset1] if bbox_idx_1 in group_idx_per_bbox: # use the existing group group_idx = group_idx_per_bbox[bbox_idx_1] else: # create new group group_idx = len(bboxes_groups) bboxes_groups.append([bbox_idx_1]) group_idx_per_bbox[bbox_idx_1] = group_idx # for every CC that occupies the same space .. for bbox_idx_2, _, _ in overlapping_bboxes[bbox_idx_1]: # if it is not in the current group, add it ... if bbox_idx_2 not in group_idx_per_bbox: # add to current group group_idx_per_bbox[bbox_idx_2] = group_idx # add to the current group bboxes_groups[group_idx].append(bbox_idx_2) else: other_group_idx = group_idx_per_bbox[bbox_idx_2] if other_group_idx != group_idx: # different group? merge .. #print("Merging groups") # for each element in the other group for other_idx_cc in bboxes_groups[other_group_idx]: # link to the current group group_idx_per_bbox[other_idx_cc] = group_idx # add to the current group bboxes_groups[group_idx].append(other_idx_cc) # leave the other group empty bboxes_groups[other_group_idx] = [] # clean up empty groups final_bbox_groups = [] final_group_idx_per_bbox = {} for group in bboxes_groups: if len(group) > 0: new_group_idx = len(final_bbox_groups) final_bbox_groups.append(group) for box_idx in group: final_group_idx_per_bbox[box_idx] = new_group_idx return final_bbox_groups, final_group_idx_per_bbox def compute_groups_temporal_information(self, bbox_groups): # compute temporal information for each group n_frames = len(self.bbox_idx_per_frame) group_ages = {} groups_per_frame = [[] for frame_idx in range(n_frames)] for group_idx, group in enumerate(bbox_groups): if len(group) == 0: continue current_ages = [] for cc_idx in group: g_first = self.unique_bbox_frames[cc_idx][0][0] g_last = self.unique_bbox_frames[cc_idx][-1][0] # add first if g_first not in current_ages: current_ages.append(g_first) if g_last not in current_ages: current_ages.append(g_last) current_ages = sorted(current_ages) group_ages[group_idx] = current_ages for frame_idx in range(current_ages[0], min(current_ages[-1] + 1, n_frames)): groups_per_frame[frame_idx].append(group_idx) return group_ages, groups_per_frame def compute_conflicting_groups(self, stable_idxs, all_overlapping_bboxes, n_groups, group_idx_per_bbox): n_stable = len(stable_idxs) # for each stable CC conflicts = {group_idx: {} for group_idx in range(n_groups)} for offset1 in range(n_stable): bbox_idx_1 = stable_idxs[offset1] # for every CC that occupies the same space (same group or not).. for bbox_idx_2, matched_pixels, size_bbox_2, size_bbox_1 in all_overlapping_bboxes[ bbox_idx_1]: # consider each link only once if bbox_idx_1 < bbox_idx_2: # UNION - Intersection unmatched_pixels = size_bbox_1 + size_bbox_2 - matched_pixels * 2 # check if they are on different groups group_idx1 = group_idx_per_bbox[bbox_idx_1] group_idx2 = group_idx_per_bbox[bbox_idx_2] if group_idx1 != group_idx2: # conflict found, add the total of matched pixels in the conflict if group_idx2 in conflicts[group_idx1]: conflicts[group_idx1][group_idx2][ "matched"] += matched_pixels conflicts[group_idx1][group_idx2][ "unmatched"] += unmatched_pixels else: conflicts[group_idx1][group_idx2] = { "matched": matched_pixels, "unmatched": unmatched_pixels } if group_idx1 in conflicts[group_idx2]: conflicts[group_idx2][group_idx1][ "matched"] += matched_pixels conflicts[group_idx2][group_idx1][ "unmatched"] += unmatched_pixels else: conflicts[group_idx2][group_idx1] = { "matched": matched_pixels, "unmatched": unmatched_pixels } return conflicts def find_container_bbox(self, bboxes): g_x1, g_y1, g_x2, g_y2 = bboxes[0] for o_x1, o_y1, o_x2, o_y2 in bboxes[1:]: g_x1 = min(g_x1, o_x1) g_y1 = min(g_y1, o_y1) g_x2 = max(g_x2, o_x2) g_y2 = max(g_y2, o_y2) return g_x1, g_y1, g_x2, g_y2 def refine_bboxes(self, bboxes_groups, group_ages, temporal_refinement): groups_bboxes = {} refined_bboxes = {} refined_per_group = {} debug_group_ids = [] for group_idx, group in enumerate(bboxes_groups): if len(group) == 0: continue # initial box ... bbox_idx = group[0] g_x1, g_y1, g_x2, g_y2 = self.unique_bbox_objects[bbox_idx] # expand to contain all images from all boxes for all frames in the current group for bbox_idx in group: # stable bbox level .. frame_idx, local_bbox_idx = self.unique_bbox_frames[bbox_idx][ 0] _, (u_x1, u_y1, u_x2, u_y2) = self.bbox_idx_per_frame[frame_idx][local_bbox_idx] # expand the boxes to get the bigger box that contains all the per-frame variations for frame_idx, local_bbox_idx in self.unique_bbox_frames[ bbox_idx]: global_bbox_idx, local_bbox = self.bbox_idx_per_frame[ frame_idx][local_bbox_idx] o_x1, o_y1, o_x2, o_y2 = local_bbox # stable bbox level ... u_x1 = min(u_x1, o_x1) u_y1 = min(u_y1, o_y1) u_x2 = max(u_x2, o_x2) u_y2 = max(u_y2, o_y2) # group level ... g_x1 = min(g_x1, o_x1) g_y1 = min(g_y1, o_y1) g_x2 = max(g_x2, o_x2) g_y2 = max(g_y2, o_y2) # print((group_idx, bbox_idx, (u_x1, u_x2, u_y1, u_y2), (g_x1, g_x2, g_y1, g_y2))) refined_bboxes[bbox_idx] = (u_x1, u_y1, u_x2, u_y2) groups_bboxes[group_idx] = (g_x1, g_y1, g_x2, g_y2) # compute the refined boxes for different temporal segments .. current_images = [] current_ages = group_ages[group_idx] if group_idx in debug_group_ids: print((group_idx, groups_bboxes[group_idx], group_ages[group_idx])) # ... for each temporal segment ... for t_segment in range(len(current_ages) - 1): t_start = current_ages[t_segment] t_end = current_ages[t_segment + 1] if temporal_refinement == BBoxStabilityEstimator.RefineAllBoxesAllSegments: # use larger group on all segments ... current_images.append([groups_bboxes[group_idx]]) else: # ... for each box in the group ... visible_bboxes = [] for bbox_idx in group: # ... check bbox_frames = [(f_idx, local_bbox_idx) for f_idx, local_bbox_idx in self.unique_bbox_frames[bbox_idx] if t_start <= f_idx <= t_end] count_bbox_frames = len(bbox_frames) # only if this box has visible frames in current time if count_bbox_frames > 0: visible_bboxes.append(refined_bboxes[bbox_idx]) # merge visible boxes that have any overlap visible_bboxes = self.spatial_box_grouping( visible_bboxes, 0.0) if temporal_refinement == BBoxStabilityEstimator.RefinePerBoxPerSegment: # use all the merged boxes separately current_images.append(visible_bboxes) else: # find container for all boxes ... current_images.append( [self.find_container_bbox(visible_bboxes)]) refined_per_group[group_idx] = current_images return refined_bboxes, groups_bboxes, refined_per_group def refined_per_frame(self, bbox_groups, groups_per_frame, group_ages, refined_per_group, save_prefix=None, stable_min_frames=3): group_next_segment = [0 for group_idx in range(len(bbox_groups))] all_reconstructed = [] for img_idx, groups_in_frame in enumerate(groups_per_frame): reconstructed = [] for group_idx in groups_in_frame: # find corresponding image .... current_ages = group_ages[group_idx] while current_ages[group_next_segment[group_idx] + 1] < img_idx: # move to the next segment group_next_segment[group_idx] += 1 # use image of selected segment ... segment_bboxes = refined_per_group[group_idx][ group_next_segment[group_idx]] # add to the image reconstructed += segment_bboxes if not save_prefix is None: # draw the reconstructed/refined boxes in white ... img_boxes = self.visualize_boxes(reconstructed, (255, 255, 255)) # add the original stable/unstable boxes img_stable = np.zeros((self.height, self.width, 3), np.uint8) img_unstable = np.zeros((self.height, self.width, 3), np.uint8) for bbox_idx, local_bbox in self.bbox_idx_per_frame[img_idx]: x1, y1, x2, y2 = local_bbox if len(self.unique_bbox_frames[bbox_idx] ) < stable_min_frames: # add unstable # reconstructed[y1:y2 + 1, x1:x2 + 1, 2] = 255 cv2.rectangle(img_unstable, (x1, y1), (x2, y2), (255, 255, 255), thickness=2) else: # add stable cv2.rectangle(img_stable, (x1, y1), (x2, y2), (255, 255, 255), thickness=2) # add stable in green channel ... img_boxes[:, :, 1] = np.bitwise_or(img_stable[:, :, 0], img_boxes[:, :, 1]) # add unstable in red channel img_boxes[:, :, 2] = np.bitwise_or(img_unstable[:, :, 0], img_boxes[:, :, 2]) cv2.imwrite(save_prefix + str(img_idx) + ".png", img_boxes) all_reconstructed.append(reconstructed) return all_reconstructed def compute_group_images(self, bboxes_groups, group_ages, binary_frames, sum_threshold, use_global): group_images = {} group_boundaries = {} debug_group_ids = [] # TODO: pending, check if grouping by ages could work ... maybe from previous process, avoid creating # very close ages ... group these maybe???? for group_idx, group in enumerate(bboxes_groups): if len(group) == 0: continue # initial box ... bbox_idx = group[0] g_x1, g_y1, g_x2, g_y2 = self.unique_bbox_objects[bbox_idx] # expand to contain all images from all boxes for all frames in the current group for bbox_idx in group: for frame_idx, local_bbox_idx in self.unique_bbox_frames[ bbox_idx]: global_bbox_idx, local_bbox = self.bbox_idx_per_frame[ frame_idx][local_bbox_idx] o_x1, o_y1, o_x2, o_y2 = local_bbox g_x1 = min(g_x1, o_x1) g_y1 = min(g_y1, o_y1) g_x2 = max(g_x2, o_x2) g_y2 = max(g_y2, o_y2) group_boundaries[group_idx] = (g_x1, g_x2, g_y1, g_y2) # size ... g_width = g_x2 - g_x1 + 1 g_height = g_y2 - g_y1 + 1 # compute the actual images using temporal information .. current_images = [] current_ages = group_ages[group_idx] g_complete_mask = np.zeros((g_height, g_width), dtype=np.int32) g_complete_sum = np.zeros((g_height, g_width), dtype=np.float64) if group_idx in debug_group_ids: print((group_idx, group_boundaries[group_idx], group_ages[group_idx])) for t_segment in range(len(current_ages) - 1): t_start = current_ages[t_segment] t_end = current_ages[t_segment + 1] g_mask = np.zeros((g_height, g_width), dtype=np.int32) g_sum = np.zeros((g_height, g_width), dtype=np.float64) # get the sum of CCs for all frames where they appear ... count_visible = 0 for bbox_idx in group: # x1, y1, x2, y2 = self.unique_bbox_objects[bbox_idx] # cc_first = self.unique_cc_frames[cc_idx][0][0] # cc_last = self.unique_cc_frames[cc_idx][-1][0] bbox_frames = [(f_idx, local_bbox_idx) for f_idx, local_bbox_idx in self.unique_bbox_frames[bbox_idx] if t_start <= f_idx < t_end] count_bbox_frames = len(bbox_frames) # only if this box has visible frames in current time if count_bbox_frames > 0: count_visible += 1 # for each frame ... for f_idx, local_bbox_idx in bbox_frames: # find the box for this frame ... _, ( x1, y1, x2, y2 ) = self.bbox_idx_per_frame[f_idx][local_bbox_idx] offset_x = x1 - g_x1 offset_y = y1 - g_y1 b_w = x2 - x1 + 1 b_h = y2 - y1 + 1 # bbox cut .... # ... image per pixel .... mask_cut = g_sum[offset_y:offset_y + b_h, offset_x:offset_x + b_w] global_mask_cut = g_complete_sum[ offset_y:offset_y + b_h, offset_x:offset_x + b_w] # print((x1, y1, x2, y2, b_w, b_h, offset_x, offset_y, g_x1, g_y1)) bin_cut = ( binary_frames[f_idx][y1:y2 + 1, x1:x2 + 1] // 255) mask_cut += bin_cut # global image ... global_mask_cut += bin_cut # ... counter per pixel ... mask_cut = g_mask[offset_y:offset_y + b_h, offset_x:offset_x + b_w] # all bounding-box pixels add as many times as frames where same box appears mask_cut += 1 # repeat for global image ... mask_cut = g_complete_mask[offset_y:offset_y + b_h, offset_x:offset_x + b_w] mask_cut += 1 # if local temporal segments are requested ... if not use_global: visible_pixels = g_mask > 0 g_sum[visible_pixels] /= g_mask[visible_pixels] g_sum[g_sum < sum_threshold] = 0.0 g_sum[g_sum >= sum_threshold] = 1.0 g_sum *= 255 segment_img = g_sum.astype(np.uint8) if group_idx in debug_group_ids: cv2.imshow( "Segment IMG #{0:d} ({1:d}-{2:d})".format( group_idx, t_start, t_end), segment_img) cv2.waitKey() # raise Exception("STOP!") current_images.append(segment_img) # TODO: test just adding all pixels from region ... g_sum = np.zeros((g_height, g_width), dtype=np.float64) for f_idx in range(t_start, t_end + 1): pass # if global temporal segments are requested ... if use_global: visible_pixels = g_complete_mask > 0 g_complete_sum[visible_pixels] /= g_complete_mask[ visible_pixels] g_complete_sum[g_complete_sum < sum_threshold] = 0.0 g_complete_sum[g_complete_sum >= sum_threshold] = 1.0 g_complete_sum *= 255 segment_img = g_complete_sum.astype(np.uint8) if group_idx in debug_group_ids or True: magnified_segment = cv2.resize( segment_img, (segment_img.shape[1] * 3, segment_img.shape[0] * 3)) cv2.imshow("complete img: #{0:d}".format(group_idx), magnified_segment) cv2.waitKey() # raise Exception("STOP!") current_images = [segment_img] * (len(current_ages) - 1) group_images[group_idx] = current_images return group_images, group_boundaries def frames_from_groups(self, cc_groups, group_boundaries, groups_per_frame, group_ages, group_images, save_prefix=None, stable_min_frames=3, show_unstable=True): group_next_segment = [0 for group_idx in range(len(cc_groups))] clean_reconstructed = [] for img_idx, groups_in_frame in enumerate(groups_per_frame): reconstructed = np.zeros((self.height, self.width, 3), dtype=np.int32) for group_idx in groups_in_frame: # find corresponding image .... current_ages = group_ages[group_idx] while current_ages[group_next_segment[group_idx] + 1] < img_idx: # move to the next segment group_next_segment[group_idx] += 1 # use image of selected segment ... segment_img = group_images[group_idx][ group_next_segment[group_idx]] # add to the image g_min_x, g_max_x, g_min_y, g_max_y = group_boundaries[ group_idx] reconstructed[g_min_y:g_max_y + 1, g_min_x:g_max_x + 1, 0] += segment_img reconstructed[g_min_y:g_max_y + 1, g_min_x:g_max_x + 1, 1] += segment_img reconstructed[g_min_y:g_max_y + 1, g_min_x:g_max_x + 1, 2] += segment_img reconstructed[reconstructed > 255] = 255 reconstructed = reconstructed.astype(np.uint8) if show_unstable: img_stable = np.zeros((self.height, self.width, 3), np.uint8) img_unstable = np.zeros((self.height, self.width, 3), np.uint8) for bbox_idx, local_bbox in self.bbox_idx_per_frame[img_idx]: x1, y1, x2, y2 = local_bbox if len(self.unique_bbox_frames[bbox_idx] ) < stable_min_frames: # add unstable # reconstructed[y1:y2 + 1, x1:x2 + 1, 2] = 255 cv2.rectangle(img_unstable, (x1, y1), (x2, y2), (255, 255, 255), thickness=2) else: # add stable cv2.rectangle(img_stable, (x1, y1), (x2, y2), (255, 255, 255), thickness=2) # add stable in green channel ... reconstructed[:, :, 1] = np.bitwise_or(img_stable[:, :, 0], reconstructed[:, :, 1]) # add unstable in red channel reconstructed[:, :, 2] = np.bitwise_or(img_unstable[:, :, 0], reconstructed[:, :, 2]) #print(group_next_segment) if not save_prefix is None: cv2.imwrite(save_prefix + str(img_idx) + ".png", reconstructed) flag, raw_data = cv2.imencode(".png", reconstructed[:, :, 0]) clean_reconstructed.append(raw_data) return clean_reconstructed
def add_frame(self, frame_bboxes): current_bboxes = self.spatial_box_grouping(frame_bboxes, self.min_combined_ratio) current_bboxes_idxs = [] if self.img_idx == 0: # simply copy all for bbox_id, bbox in enumerate(current_bboxes): # add the box to list of unique boxes ... self.unique_bbox_objects.append(bbox) # frames on which the bbox appears, raw label assigned to that CC self.unique_bbox_frames.append([(0, bbox_id)]) bbox_idx = len(self.unique_bbox_objects) - 1 current_bboxes_idxs.append((bbox_idx, bbox)) self.bbox_last_frame.append(0) self.bbox_active.append(bbox_idx) # add to indices ... x1, y1, x2, y2 = bbox self.bbox_int_index_x.add(x1, x2, bbox_idx) self.bbox_int_index_y.add(y1, y2, bbox_idx) else: # create indices for current bboxes other_index_x = IntervalIndex(True) other_index_y = IntervalIndex(True) for bbox_idx, (x1, y1, x2, y2) in enumerate(current_bboxes): other_index_x.add(x1, x2, bbox_idx) other_index_y.add(y1, y2, bbox_idx) # compute CC with matching regions set_x = set(other_index_x.find_matches(self.bbox_int_index_x)) set_y = set(other_index_y.find_matches(self.bbox_int_index_y)) # list of all pairs of CC with intersecting intervals in X and Y merged = sorted(list(set_x.intersection(set_y))) self.tempo_count += len(merged) # check every matching CC pre_add_size = len(self.bbox_active) next_match_idx = 0 for bbox_idx, bbox in enumerate(current_bboxes): found = False # check all matches in the list of matches for current CC while next_match_idx < len( merged) and merged[next_match_idx][0] == bbox_idx: if not found: prev_idx = merged[next_match_idx][1] prev_bbox = self.unique_bbox_objects[prev_idx] bbox_IOU = self.get_bboxes_IOU(bbox, prev_bbox) # print(bbox_IOU) if bbox_IOU >= self.min_temporal_IOU: # assume they are equivalent found = True self.unique_bbox_frames[prev_idx].append( (self.img_idx, bbox_idx)) current_bboxes_idxs.append((prev_idx, bbox)) # update last frame seen for this cc... self.bbox_last_frame[prev_idx] = self.img_idx next_match_idx += 1 # Not match was found? if not found: # add self.unique_bbox_objects.append(bbox) self.unique_bbox_frames.append([(self.img_idx, bbox_idx)]) new_bbox_idx = len(self.unique_bbox_objects) - 1 current_bboxes_idxs.append((new_bbox_idx, bbox)) self.bbox_last_frame.append(self.img_idx) self.bbox_active.append(new_bbox_idx) # add to indices ... x1, y1, x2, y2 = bbox self.bbox_int_index_x.add(x1, x2, new_bbox_idx) self.bbox_int_index_y.add(y1, y2, new_bbox_idx) # remove CC that are no longer active pre_remove_size = len(self.bbox_active) tempo_pos = 0 while tempo_pos < len(self.bbox_active): bbox_idx = self.bbox_active[tempo_pos] if self.img_idx - self.bbox_last_frame[ bbox_idx] >= self.max_gap: # no longer active .. # delete from active list del self.bbox_active[tempo_pos] # delete from interval indices bbox = self.unique_bbox_objects[bbox_idx] x1, y1, x2, y2 = bbox self.bbox_int_index_x.remove(x1, x2, bbox_idx) self.bbox_int_index_y.remove(y1, y2, bbox_idx) #print self.cc_last_frame[cc_idx], else: # still active tempo_pos += 1 """ total_added = pre_remove_size - pre_add_size total_removed = pre_remove_size - len(self.bbox_active) msg = "{0:d} , (Added: {1:d}, Removed: {2:d})".format(len(self.bbox_active), total_added, total_removed) print(msg) """ self.bbox_idx_per_frame.append(current_bboxes_idxs) self.img_idx += 1 if self.verbose: msg = "[{0:d} ({1:d}, {2:d})]".format( self.img_idx, len(current_bboxes), len(self.unique_bbox_objects)) print(msg, end="\r")
def spatial_box_grouping(self, frame_bboxes, min_combined_ratio=None): if min_combined_ratio is None: min_combined_ratio = self.min_combined_ratio current_boxes = list(frame_bboxes) merged_boxes = True while merged_boxes: merged_boxes = False # sort bounding boxes by descending size ... boxes_by_size = [] for x1, y1, x2, y2 in current_boxes: w = x2 - x1 + 1 h = y2 - y1 + 1 area = w * h boxes_by_size.append((area, (x1, y1, x2, y2))) boxes_by_size = sorted(boxes_by_size, reverse=True, key=lambda x: x[0]) # print(boxes_by_size) # create interval index to find matches quicker (all boxes against all boxes from same frame) int_index_x = IntervalIndex(True) int_index_y = IntervalIndex(True) for box_idx, (area, (x1, y1, x2, y2)) in enumerate(boxes_by_size): int_index_x.add(x1, x2 + 1, box_idx) int_index_y.add(y1, y2 + 1, box_idx) # ... find pair-wise matches ... set_x = set(int_index_x.find_matches(int_index_x)) set_y = set(int_index_y.find_matches(int_index_y)) # .... list of all pairs of boxes with intersecting intervals in X and Y merge_candidates = sorted(list(set_x.intersection(set_y))) # ... filter self-matches and repetitions ... merge_candidates = [(box_idx1, box_idx2) for box_idx1, box_idx2 in merge_candidates if box_idx1 < box_idx2] # ... split by first box .. candidates_by_box = {idx: [] for idx in range(len(boxes_by_size))} for box_idx1, box_idx2 in merge_candidates: candidates_by_box[box_idx1].append(box_idx2) # print(merge_candidates) # print(candidates_by_box) box_added = [False] * len(boxes_by_size) current_boxes = [] # for each box (sorted by size) for box_idx in range(len(boxes_by_size)): # if this box has been previously added (merged with ealier box) if box_added[box_idx]: # skip ... continue box_added[box_idx] = True # current box boundaries c_area, (c_x1, c_y1, c_x2, c_y2) = boxes_by_size[box_idx] for second_box_idx in candidates_by_box[box_idx]: if box_added[second_box_idx]: # skip merge candidate ... continue # get box boundaries ... o_area, (o_x1, o_y1, o_x2, o_y2) = boxes_by_size[second_box_idx] comb_area_ratio = self.get_combined_box_area_ratio( (c_x1, c_y1, c_x2, c_y2), (o_x1, o_y1, o_x2, o_y2)) # print(((c_x1, c_y1, c_x2, c_y2), boxes_by_size[second_box_idx], comb_area_ratio, box_idx, second_box_idx)) if comb_area_ratio >= min_combined_ratio: # merge! # expand current bounding box to include the smaller box ... c_x1 = min(c_x1, o_x1) c_y1 = min(c_y1, o_y1) c_x2 = max(c_x2, o_x2) c_y2 = max(c_y2, o_y2) # mark second box as added, so it won't be added to the current list .. box_added[second_box_idx] = True merged_boxes = True # add to the next set of accepted boxes current_boxes.append((c_x1, c_y1, c_x2, c_y2)) """ if len(frame_bboxes) > 30: original_img = self.visualize_boxes(frame_bboxes, (255, 0, 0)) final_img = self.visualize_boxes(current_boxes, (0, 255, 0)) final_img[:, :, 0] = original_img[:, :, 0] debug_img = cv2.resize(final_img,(960, 540)) cv2.imshow("check", debug_img) cv2.waitKey() raise Exception("Error!") """ return current_boxes
class CCStabilityEstimator: def __init__(self, width, height, min_recall, min_precision, max_gap, verbose=False): self.width = width self.height = height self.min_recall = min_recall self.min_precision = min_precision self.max_gap = max_gap self.unique_cc_objects = [] self.unique_cc_frames = [] self.cc_idx_per_frame = [] self.cc_int_index_x = IntervalIndex(True) self.cc_int_index_y = IntervalIndex(True) self.fake_age = np.zeros((height, width), dtype=np.float32) self.img_idx = 0 self.tempo_count = 0 # optimizing the matching ... self.cc_last_frame = [] self.cc_active = [] self.verbose = verbose def get_raw_cc_count(self): total = 0 for current_frame in self.cc_idx_per_frame: total += len(current_frame) return total def add_frame(self, img, input_binary=False): # get the CC if input_binary: # use given binary binary = img else: # binarize binary = Binarizer.backgroundSubtractionBinarization(img.astype('uint8')) current_cc = Labeler.extractSpatioTemporalContent(binary, self.fake_age) current_cc_idxs = [] if self.img_idx == 0: # simply copy all for cc in current_cc: self.unique_cc_objects.append(cc) # CC objet # frames on which the CC appears, raw label assigend to that CC self.unique_cc_frames.append([(0, cc.cc_id + 1)]) cc_idx = len(self.unique_cc_objects) - 1 current_cc_idxs.append((cc_idx, cc)) self.cc_last_frame.append(0) self.cc_active.append(cc_idx) # add to indices ... self.cc_int_index_x.add(cc.min_x, cc.max_x + 1, cc_idx) self.cc_int_index_y.add(cc.min_y, cc.max_y + 1, cc_idx) else: # create indices for current CC other_index_x = IntervalIndex(True) other_index_y = IntervalIndex(True) for cc_idx, cc in enumerate(current_cc): other_index_x.add(cc.min_x, cc.max_x + 1, cc_idx) other_index_y.add(cc.min_y, cc.max_y + 1, cc_idx) # compute CC with matching regions set_x = set(other_index_x.find_matches(self.cc_int_index_x)) set_y = set(other_index_y.find_matches(self.cc_int_index_y)) # list of all pairs of CC with intersecting intervals in X and Y merged = sorted(list(set_x.intersection(set_y))) self.tempo_count += len(merged) # check every matching CC pre_add_size = len(self.cc_active) next_match_idx = 0 for cc_idx, cc in enumerate(current_cc): found = False # check all matches in the list of matches for current CC while next_match_idx < len(merged) and merged[next_match_idx][0] == cc_idx: if not found: prev_idx = merged[next_match_idx][1] prev_cc = self.unique_cc_objects[prev_idx] recall, precision = cc.getOverlapFMeasure(prev_cc, False, False) if recall >= self.min_recall and precision >= self.min_precision: # assume they are equivalent found = True self.unique_cc_frames[prev_idx].append((self.img_idx, cc.cc_id + 1)) current_cc_idxs.append((prev_idx, cc)) # update last frame seen for this cc... self.cc_last_frame[prev_idx] = self.img_idx next_match_idx += 1 # Not match was found? if not found: # add self.unique_cc_objects.append(cc) self.unique_cc_frames.append([(self.img_idx, cc.cc_id + 1)]) new_cc_idx = len(self.unique_cc_objects) - 1 current_cc_idxs.append((new_cc_idx, cc)) self.cc_last_frame.append(self.img_idx) self.cc_active.append(new_cc_idx) # add to indices ... self.cc_int_index_x.add(cc.min_x, cc.max_x + 1, new_cc_idx) self.cc_int_index_y.add(cc.min_y, cc.max_y + 1, new_cc_idx) # remove CC that are no longer active pre_remove_size = len(self.cc_active) tempo_pos = 0 while tempo_pos < len(self.cc_active): cc_idx = self.cc_active[tempo_pos] if self.img_idx - self.cc_last_frame[cc_idx] >= self.max_gap: # no longer active .. # delete from active list del self.cc_active[tempo_pos] # delete from interval indices cc = self.unique_cc_objects[cc_idx] self.cc_int_index_x.remove(cc.min_x, cc.max_x + 1, cc_idx) self.cc_int_index_y.remove(cc.min_y, cc.max_y + 1, cc_idx) #print self.cc_last_frame[cc_idx], else: # still active tempo_pos += 1 #print(str(len(self.cc_active)) + ", (Added: " + str(pre_remove_size - pre_add_size) + ", Removed: " + str(pre_remove_size - len(self.cc_active)) + ")") self.cc_idx_per_frame.append(current_cc_idxs) self.img_idx += 1 if self.verbose: print("[" + str(self.img_idx) + " (" + str(len(current_cc)) + ", " + str(len(self.unique_cc_objects)) + ")]", end="\r") def finish_processing(self): if self.verbose: print(".") print("Erase this final count (after done): " + str(self.tempo_count)) self.fake_age = None def rebuilt_binary_images(self): rebuilt_frames = [] for frame_ccs in self.cc_idx_per_frame: binary = np.zeros((self.height, self.width), dtype=np.uint8) for idx_cc, local_cc in frame_ccs: binary[local_cc.min_y:local_cc.max_y +1, local_cc.min_x:local_cc.max_x + 1] += local_cc.img rebuilt_frames.append(binary) return rebuilt_frames def split_stable_cc_by_gaps(self, max_gap, stable_min_frames): splitted_count = 0 n_original_objects = len(self.unique_cc_objects) for idx_cc in range(n_original_objects): current_frames = self.unique_cc_frames[idx_cc] n_local = len(current_frames) current_group = [current_frames[0]] valid_groups = [current_group] for frame_offset in range(1, n_local): curr_frame_idx = current_frames[frame_offset][0] prev_frame_idx = current_frames[frame_offset - 1][0] current_gap = curr_frame_idx - prev_frame_idx if current_gap > max_gap: # not acceptable gap .. current_group = [current_frames[frame_offset]] valid_groups.append(current_group) else: # acceptable current_group.append(current_frames[frame_offset]) if len(valid_groups) >= 2 and n_local >= stable_min_frames: # replace the frames in the original cc list to only the first group .. self.unique_cc_frames[idx_cc] = valid_groups[0] # for each group (new CC) for group_offset in range(1, len(valid_groups)): new_idx_cc = len(self.unique_cc_objects) # add another reference to the original CC self.unique_cc_objects.append(self.unique_cc_objects[idx_cc]) # add CC frame reference ... self.unique_cc_frames.append(valid_groups[group_offset]) # for each frame where the orgiianl appeared .. for frame_idx, local_cc in valid_groups[group_offset]: # find the CC on the frame ... for offset, (local_cc_idx, local_cc) in enumerate(self.cc_idx_per_frame[frame_idx]): if local_cc_idx == idx_cc: # replace self.cc_idx_per_frame[frame_idx][offset] = (new_idx_cc, local_cc) break splitted_count += 1 return splitted_count def get_stable_cc_idxs(self, min_stable_frames): stable_idxs = [] for idx_cc in range(len(self.unique_cc_objects)): if len(self.unique_cc_frames[idx_cc]) >= min_stable_frames: stable_idxs.append(idx_cc) return stable_idxs def get_temporal_index(self): temporal_index = [] for idxs_per_frame in self.cc_idx_per_frame: temporal_index.append([cc_idx for cc_idx, local_cc in idxs_per_frame]) return temporal_index def compute_overlapping_stable_cc(self, stable_idxs, temporal_window): n_objects = len(self.unique_cc_objects) n_stable = len(stable_idxs) all_overlapping_cc = [[] for x in range(n_objects)] time_overlapping_cc = [[] for x in range(n_objects)] total_intersections = 0 for offset1 in range(n_stable): # get first stable idx_cc1 = stable_idxs[offset1] cc1 = self.unique_cc_objects[idx_cc1] cc1_t_start = self.unique_cc_frames[idx_cc1][0][0] cc1_t_end = self.unique_cc_frames[idx_cc1][-1][0] print("Processing: " + str(offset1),end="\r") for offset2 in range(offset1 + 1, n_stable): idx_cc2 = stable_idxs[offset2] cc2 = self.unique_cc_objects[idx_cc2] cc2_t_start = self.unique_cc_frames[idx_cc2][0][0] cc2_t_end = self.unique_cc_frames[idx_cc2][-1][0] # check intersection in space recall, precision = cc1.getOverlapFMeasure(cc2, False, False) if recall > 0.0 or precision > 0.0: matched_pixels = int(cc1.size * recall) all_overlapping_cc[idx_cc1].append((idx_cc2, matched_pixels, cc2.size, cc1.size)) all_overlapping_cc[idx_cc2].append((idx_cc1, matched_pixels, cc1.size, cc2.size)) # now, check intersection in time (considering time window) if cc1_t_end + temporal_window >= cc2_t_start and cc2_t_end >= cc1_t_start - temporal_window: # they have some intersection in pixels.... time_overlapping_cc[idx_cc1].append((idx_cc2, recall, precision)) time_overlapping_cc[idx_cc2].append((idx_cc1, precision, recall)) total_intersections += 1 return time_overlapping_cc, total_intersections, all_overlapping_cc def compute_groups(self, stable_idxs, overlapping_cc): n_stable = len(stable_idxs) # Determine Groups of CC's that coexist in time and space and treat them as single units... cc_groups = [] group_idx_per_cc = {} for offset1 in range(n_stable): idx_cc1 = stable_idxs[offset1] if idx_cc1 in group_idx_per_cc: # use the existing group group_idx = group_idx_per_cc[idx_cc1] else: # create new group group_idx = len(cc_groups) cc_groups.append([idx_cc1]) group_idx_per_cc[idx_cc1] = group_idx # for every CC that occupies the same space .. for idx_cc2, recall, precision in overlapping_cc[idx_cc1]: # if it is not in the current group, add it ... if idx_cc2 not in group_idx_per_cc: # add to current group group_idx_per_cc[idx_cc2] = group_idx # add to the current group cc_groups[group_idx].append(idx_cc2) else: other_group_idx = group_idx_per_cc[idx_cc2] if other_group_idx != group_idx: # different group? merge .. #print("Merging groups") # for each element in the other group for other_idx_cc in cc_groups[other_group_idx]: # link to the current group group_idx_per_cc[other_idx_cc] = group_idx # add to the current group cc_groups[group_idx].append(other_idx_cc) # leave the other group empty cc_groups[other_group_idx] = [] # clean up empty groups final_cc_groups = [] final_group_idx_per_cc = {} for group in cc_groups: if len(group) > 0: new_group_idx = len(final_cc_groups) final_cc_groups.append(group) for idx_cc in group: final_group_idx_per_cc[idx_cc] = new_group_idx return final_cc_groups, final_group_idx_per_cc def compute_groups_temporal_information(self, cc_groups): # compute temporal information for each group n_frames = len(self.cc_idx_per_frame) group_ages = {} groups_per_frame = [[] for frame_idx in range(n_frames)] for group_idx, group in enumerate(cc_groups): if len(group) == 0: continue current_ages = [] for cc_idx in group: g_first = self.unique_cc_frames[cc_idx][0][0] g_last = self.unique_cc_frames[cc_idx][-1][0] # add first if g_first not in current_ages: current_ages.append(g_first) if g_last not in current_ages: current_ages.append(g_last) current_ages = sorted(current_ages) group_ages[group_idx] = current_ages for frame_idx in range(current_ages[0], min(current_ages[-1] + 1, n_frames)): groups_per_frame[frame_idx].append(group_idx) return group_ages, groups_per_frame def compute_conflicting_groups(self, stable_idxs, all_overlapping_cc, n_groups, group_idx_per_cc): n_stable = len(stable_idxs) # for each stable CC conflicts = {group_idx:{} for group_idx in range(n_groups)} for offset1 in range(n_stable): idx_cc1 = stable_idxs[offset1] # for every CC that occupies the same space (same group or not).. for idx_cc2, matched_pixels, size_cc2, size_cc1 in all_overlapping_cc[idx_cc1]: # consider each link only once if idx_cc1 < idx_cc2: unmatched_pixels = size_cc1 + size_cc2 - matched_pixels * 2 # check if they are on different groups group_idx1 = group_idx_per_cc[idx_cc1] group_idx2 = group_idx_per_cc[idx_cc2] if group_idx1 != group_idx2: # conflict found, add the total of matched pixels in the conflict if group_idx2 in conflicts[group_idx1]: conflicts[group_idx1][group_idx2]["matched"] += matched_pixels conflicts[group_idx1][group_idx2]["unmatched"] += unmatched_pixels else: conflicts[group_idx1][group_idx2] = { "matched": matched_pixels, "unmatched": unmatched_pixels } if group_idx1 in conflicts[group_idx2]: conflicts[group_idx2][group_idx1]["matched"] += matched_pixels conflicts[group_idx2][group_idx1]["unmatched"] += unmatched_pixels else: conflicts[group_idx2][group_idx1] = { "matched": matched_pixels, "unmatched": unmatched_pixels } return conflicts def compute_group_images_from_raw_binary(self, cc_groups, group_ages, binary_frames, segment_threshold): group_images = {} group_boundaries = {} for group_idx, group in enumerate(cc_groups): if len(group) == 0: continue # first, compute the combined boundaries... cc_idx = group[0] g_min_x = self.unique_cc_objects[cc_idx].min_x g_max_x = self.unique_cc_objects[cc_idx].max_x g_min_y = self.unique_cc_objects[cc_idx].min_y g_max_y = self.unique_cc_objects[cc_idx].max_y for cc_idx in group: g_min_x = min(g_min_x, self.unique_cc_objects[cc_idx].min_x) g_max_x = max(g_max_x, self.unique_cc_objects[cc_idx].max_x) g_min_y = min(g_min_y, self.unique_cc_objects[cc_idx].min_y) g_max_y = max(g_max_y, self.unique_cc_objects[cc_idx].max_y) group_boundaries[group_idx] = (g_min_x, g_max_x, g_min_y, g_max_y) # size ... g_width = g_max_x - g_min_x + 1 g_height = g_max_y - g_min_y + 1 # compute the actual images using temporal information .. current_images = [] current_ages = group_ages[group_idx] for t_segment in range(len(current_ages) - 1): t_start = current_ages[t_segment] t_end = current_ages[t_segment + 1] g_mask = np.zeros((g_height, g_width), dtype=np.int32) for cc_idx in group: cc = self.unique_cc_objects[cc_idx] cc_first = self.unique_cc_frames[cc_idx][0][0] cc_last = self.unique_cc_frames[cc_idx][-1][0] # check if cc existed in current time segment ... if cc_first <= t_end and t_start <= cc_last: # add to current image offset_x = cc.min_x - g_min_x offset_y = cc.min_y - g_min_y g_mask[offset_y:offset_y + cc.getHeight(), offset_x:offset_x + cc.getWidth()] += (cc.img // 255) # now get the mask ... g_mask = (g_mask > 0).astype('uint8') * 255 # use the mask to obtain the image of the group for the current segment of time segment_img = np.zeros((g_height, g_width), dtype=np.int32) for frame_idx in range(t_start, t_end + 1): local_patch = np.bitwise_and(binary_frames[frame_idx][g_min_y:g_max_y + 1, g_min_x:g_max_x + 1], g_mask) // 255 segment_img += local_patch segment_img = (segment_img * 255) // segment_img.max() #segment_img[segment_img < min_pixel_val] = 0 segment_img = (segment_img > segment_threshold).astype(dtype=np.uint8) * 255 #g_img = (g_img * 255) / g_img.max() #cv2.imwrite("output/images/fs_a_group_" + str(group_idx) + "_" + str(t_start) + ".png", segment_img) current_images.append(segment_img) group_images[group_idx] = current_images return group_images, group_boundaries def compute_group_images(self, cc_groups, group_ages, segment_threshold): group_images = {} group_boundaries = {} for group_idx, group in enumerate(cc_groups): if len(group) == 0: continue # first, compute the combined boundaries... cc_idx = group[0] g_min_x = self.unique_cc_objects[cc_idx].min_x g_max_x = self.unique_cc_objects[cc_idx].max_x g_min_y = self.unique_cc_objects[cc_idx].min_y g_max_y = self.unique_cc_objects[cc_idx].max_y for cc_idx in group: g_min_x = min(g_min_x, self.unique_cc_objects[cc_idx].min_x) g_max_x = max(g_max_x, self.unique_cc_objects[cc_idx].max_x) g_min_y = min(g_min_y, self.unique_cc_objects[cc_idx].min_y) g_max_y = max(g_max_y, self.unique_cc_objects[cc_idx].max_y) group_boundaries[group_idx] = (g_min_x, g_max_x, g_min_y, g_max_y) # size ... g_width = g_max_x - g_min_x + 1 g_height = g_max_y - g_min_y + 1 # compute the actual images using temporal information .. current_images = [] current_ages = group_ages[group_idx] for t_segment in range(len(current_ages) - 1): t_start = current_ages[t_segment] t_end = current_ages[t_segment + 1] g_mask = np.zeros((g_height, g_width), dtype=np.int32) # get the sum of CCs for all frames where they appear ... for cc_idx in group: cc = self.unique_cc_objects[cc_idx] # cc_first = self.unique_cc_frames[cc_idx][0][0] # cc_last = self.unique_cc_frames[cc_idx][-1][0] cc_frames = len([f_idx for f_idx, _ in self.unique_cc_frames[cc_idx] if t_start <= f_idx <= t_end]) if cc_frames > 0: offset_x = cc.min_x - g_min_x offset_y = cc.min_y - g_min_y mask_cut = g_mask[offset_y:offset_y + cc.getHeight(), offset_x:offset_x + cc.getWidth()] # all CC pixels addes as many times as frames the entire CC appears mask_cut += (cc.img // 255) * cc_frames segment_img = ((g_mask.astype(np.float64) / g_mask.max()) >= segment_threshold).astype(np.uint8) * 255 current_images.append(segment_img) group_images[group_idx] = current_images return group_images, group_boundaries def frames_from_groups(self, cc_groups, group_boundaries, groups_per_frame, group_ages, group_images, save_prefix=None, stable_min_frames=3, show_unstable=True): group_next_segment = [0 for group_idx in range(len(cc_groups))] clean_binary = [] for img_idx, groups_in_frame in enumerate(groups_per_frame): reconstructed = np.zeros((self.height, self.width, 3), dtype=np.uint8) for group_idx in groups_in_frame: # find corresponding image .... current_ages = group_ages[group_idx] while current_ages[group_next_segment[group_idx] + 1] < img_idx: # move to the next segment group_next_segment[group_idx] += 1 # use image of selected segment ... segment_img = group_images[group_idx][group_next_segment[group_idx]] # add to the image g_min_x, g_max_x, g_min_y, g_max_y = group_boundaries[group_idx] reconstructed[g_min_y:g_max_y + 1, g_min_x:g_max_x + 1, 0] += segment_img reconstructed[g_min_y:g_max_y + 1, g_min_x:g_max_x + 1, 1] += segment_img if not show_unstable: reconstructed[g_min_y:g_max_y + 1, g_min_x:g_max_x + 1, 2] += segment_img if show_unstable: for cc_idx, local_cc in self.cc_idx_per_frame[img_idx]: if len(self.unique_cc_frames[cc_idx]) < stable_min_frames: # add unstable in the red channel cc = local_cc reconstructed[cc.min_y:cc.max_y +1, cc.min_x:cc.max_x + 1, 2] += cc.img #print(group_next_segment) if not save_prefix is None: cv2.imwrite(save_prefix + "_stab_" + str(img_idx) + ".png", reconstructed) cv2.imwrite(save_prefix + "_clean_" + str(img_idx) + ".png", reconstructed[:,:, 0]) flag, raw_data = cv2.imencode(".png", reconstructed[:,:, 0]) clean_binary.append(raw_data) return clean_binary @staticmethod def find_block_stable_cc(block_images, min_f_score, min_stable_bg = 0.50, verbose=False): n_points, height, width = block_images.shape estimator = CCStabilityEstimator(width, height, min_f_score, min_stable_bg, verbose) for img_idx in range(n_points): img = block_images[img_idx, :, :] estimator.add_frame(img) return estimator.unique_cc_objects, estimator.unique_cc_frames, estimator.cc_idx_per_frame, estimator.stable_background_mask @staticmethod def compute_overlapping_CC_groups(cc_objects): n_objects = len(cc_objects) all_overlapping_cc = [[x] for x in range(n_objects)] # compute all pairwise overlaps for idx1 in range(n_objects): # get first stable cc1 = cc_objects[idx1] for idx2 in range(idx1 + 1, n_objects): cc2 = cc_objects[idx2] # check intersection in space recall, precision = cc1.getOverlapFMeasure(cc2, False, False) if recall > 0.0 or precision > 0.0: all_overlapping_cc[idx1].append(idx2) all_overlapping_cc[idx2].append(idx1) # find groups containing pairs of overlapping objects (transitive overlap) group_overlap_idx = [x for x in range(n_objects)] merged_groups = {x: {x} for x in range(n_objects)} for idx in range(n_objects): # check the group of the current object merged_idx1 = group_overlap_idx[idx] current_group = all_overlapping_cc[idx] for other_idx in current_group[1:]: # check the group of an overlapping obejct merged_idx2 = group_overlap_idx[other_idx] # if it is different, then merge! if merged_idx1 != merged_idx2: # 1 )create a single larger group merged_groups[merged_idx1] = merged_groups[merged_idx1].union(merged_groups[merged_idx2]) # 2 ) Redirect each element on the group that will disappear, to the newer larger group for old_group_idx in merged_groups[merged_idx2]: group_overlap_idx[old_group_idx] = merged_idx1 # 3) Delete old unreferenced group del merged_groups[merged_idx2] # finally, split by overlapping and no overlapping .. overlapping_groups = [] no_overlaps = [] for group_idx in merged_groups: merged_list = list(merged_groups[group_idx]) if len(merged_list) == 1: no_overlaps.append(merged_list[0]) else: overlapping_groups.append(merged_list) return overlapping_groups, no_overlaps
def add_frame(self, img, input_binary=False): # get the CC if input_binary: # use given binary binary = img else: # binarize binary = Binarizer.backgroundSubtractionBinarization(img.astype('uint8')) current_cc = Labeler.extractSpatioTemporalContent(binary, self.fake_age) current_cc_idxs = [] if self.img_idx == 0: # simply copy all for cc in current_cc: self.unique_cc_objects.append(cc) # CC objet # frames on which the CC appears, raw label assigend to that CC self.unique_cc_frames.append([(0, cc.cc_id + 1)]) cc_idx = len(self.unique_cc_objects) - 1 current_cc_idxs.append((cc_idx, cc)) self.cc_last_frame.append(0) self.cc_active.append(cc_idx) # add to indices ... self.cc_int_index_x.add(cc.min_x, cc.max_x + 1, cc_idx) self.cc_int_index_y.add(cc.min_y, cc.max_y + 1, cc_idx) else: # create indices for current CC other_index_x = IntervalIndex(True) other_index_y = IntervalIndex(True) for cc_idx, cc in enumerate(current_cc): other_index_x.add(cc.min_x, cc.max_x + 1, cc_idx) other_index_y.add(cc.min_y, cc.max_y + 1, cc_idx) # compute CC with matching regions set_x = set(other_index_x.find_matches(self.cc_int_index_x)) set_y = set(other_index_y.find_matches(self.cc_int_index_y)) # list of all pairs of CC with intersecting intervals in X and Y merged = sorted(list(set_x.intersection(set_y))) self.tempo_count += len(merged) # check every matching CC pre_add_size = len(self.cc_active) next_match_idx = 0 for cc_idx, cc in enumerate(current_cc): found = False # check all matches in the list of matches for current CC while next_match_idx < len(merged) and merged[next_match_idx][0] == cc_idx: if not found: prev_idx = merged[next_match_idx][1] prev_cc = self.unique_cc_objects[prev_idx] recall, precision = cc.getOverlapFMeasure(prev_cc, False, False) if recall >= self.min_recall and precision >= self.min_precision: # assume they are equivalent found = True self.unique_cc_frames[prev_idx].append((self.img_idx, cc.cc_id + 1)) current_cc_idxs.append((prev_idx, cc)) # update last frame seen for this cc... self.cc_last_frame[prev_idx] = self.img_idx next_match_idx += 1 # Not match was found? if not found: # add self.unique_cc_objects.append(cc) self.unique_cc_frames.append([(self.img_idx, cc.cc_id + 1)]) new_cc_idx = len(self.unique_cc_objects) - 1 current_cc_idxs.append((new_cc_idx, cc)) self.cc_last_frame.append(self.img_idx) self.cc_active.append(new_cc_idx) # add to indices ... self.cc_int_index_x.add(cc.min_x, cc.max_x + 1, new_cc_idx) self.cc_int_index_y.add(cc.min_y, cc.max_y + 1, new_cc_idx) # remove CC that are no longer active pre_remove_size = len(self.cc_active) tempo_pos = 0 while tempo_pos < len(self.cc_active): cc_idx = self.cc_active[tempo_pos] if self.img_idx - self.cc_last_frame[cc_idx] >= self.max_gap: # no longer active .. # delete from active list del self.cc_active[tempo_pos] # delete from interval indices cc = self.unique_cc_objects[cc_idx] self.cc_int_index_x.remove(cc.min_x, cc.max_x + 1, cc_idx) self.cc_int_index_y.remove(cc.min_y, cc.max_y + 1, cc_idx) #print self.cc_last_frame[cc_idx], else: # still active tempo_pos += 1 #print(str(len(self.cc_active)) + ", (Added: " + str(pre_remove_size - pre_add_size) + ", Removed: " + str(pre_remove_size - len(self.cc_active)) + ")") self.cc_idx_per_frame.append(current_cc_idxs) self.img_idx += 1 if self.verbose: print("[" + str(self.img_idx) + " (" + str(len(current_cc)) + ", " + str(len(self.unique_cc_objects)) + ")]", end="\r")