|
32 | 32 | # crossover barely moves with N, so it is a degree and not a pair budget. |
33 | 33 | NMM_MAX_DEGREE = 40.0 |
34 | 34 |
|
| 35 | +# NMS and greedy NMM only ever read rows of boxes that survived, so they settle |
| 36 | +# far fewer rows than NMM and the stored list keeps paying off much longer. Both |
| 37 | +# measure the same crossover, around degree 175; staying under it keeps the pair |
| 38 | +# list bounded on the crowded inputs streaming exists to protect. |
| 39 | +SURVIVOR_MAX_DEGREE = 150.0 |
| 40 | + |
35 | 41 | # Probing the tree in one call would return sample x degree pairs at once, which |
36 | 42 | # on crowded boxes dwarfs everything the path being chosen goes on to allocate. |
37 | 43 | # Only the count is wanted, so the probe is spent a chunk at a time. |
@@ -96,17 +102,31 @@ def _dominates(left: int | np.ndarray, right: np.ndarray, scores: np.ndarray, bo |
96 | 102 | return lower_score | (score_equal & ~left_lt) |
97 | 103 |
|
98 | 104 |
|
99 | | -def should_stream_nmm(boxes: np.ndarray) -> bool: |
100 | | - """Return whether NMM should answer rows from the tree instead of storing pairs. |
| 105 | +def should_stream(boxes: np.ndarray, max_degree: float) -> bool: |
| 106 | + """Return whether to answer rows from the tree instead of storing pairs. |
101 | 107 |
|
102 | 108 | Args: |
103 | 109 | boxes: Array of shape (N, 4) with columns [x1, y1, x2, y2]. |
| 110 | + max_degree: Mean degree above which streaming wins, per caller. |
104 | 111 |
|
105 | 112 | Returns: |
106 | 113 | True when the boxes intersect each other often enough that reading rows |
107 | 114 | from the tree beats storing them. |
108 | 115 | """ |
109 | | - return estimate_mean_degree(boxes) > NMM_MAX_DEGREE |
| 116 | + return estimate_mean_degree(boxes) > max_degree |
| 117 | + |
| 118 | + |
| 119 | +def should_stream_nmm(boxes: np.ndarray) -> bool: |
| 120 | + """Return whether NMM should answer rows from the tree instead of storing pairs.""" |
| 121 | + return should_stream(boxes, NMM_MAX_DEGREE) |
| 122 | + |
| 123 | + |
| 124 | +def should_stream_survivor(boxes: np.ndarray) -> bool: |
| 125 | + """Return whether NMS and greedy NMM should read rows from the tree. |
| 126 | +
|
| 127 | + Both settle a row only for a box that survived, so they share a crossover. |
| 128 | + """ |
| 129 | + return should_stream(boxes, SURVIVOR_MAX_DEGREE) |
110 | 130 |
|
111 | 131 |
|
112 | 132 | def should_use_sparse(n: int, match_threshold: float) -> bool: |
@@ -383,6 +403,70 @@ def _dominates_all(indptr: np.ndarray, indices: np.ndarray, scores: np.ndarray, |
383 | 403 | return _dominates(rows, indices, scores, boxes) |
384 | 404 |
|
385 | 405 |
|
| 406 | +def nms_sparse(indptr: np.ndarray, indices: np.ndarray, sorted_idxs: np.ndarray) -> list[int]: |
| 407 | + """NMS over a CSR match adjacency. Mirrors ``nms_from_matrix``. |
| 408 | +
|
| 409 | + Suppressing a whole row is one vectorized store, so while the pair list |
| 410 | + stays bounded this beats querying the tree per survivor. |
| 411 | +
|
| 412 | + Args: |
| 413 | + indptr: CSR row pointers of length N + 1. |
| 414 | + indices: CSR column indices. |
| 415 | + sorted_idxs: Indices sorted by score descending. |
| 416 | +
|
| 417 | + Returns: |
| 418 | + List of kept indices sorted by score descending. |
| 419 | + """ |
| 420 | + keep: list[int] = [] |
| 421 | + suppressed = np.zeros(len(indptr) - 1, dtype=bool) |
| 422 | + |
| 423 | + for idx in sorted_idxs: |
| 424 | + if suppressed[idx]: |
| 425 | + continue |
| 426 | + keep.append(int(idx)) |
| 427 | + suppressed[indices[indptr[idx] : indptr[idx + 1]]] = True |
| 428 | + |
| 429 | + return keep |
| 430 | + |
| 431 | + |
| 432 | +def greedy_nmm_sparse( |
| 433 | + indptr: np.ndarray, |
| 434 | + indices: np.ndarray, |
| 435 | + sorted_idxs: np.ndarray, |
| 436 | +) -> dict[int, list[int]]: |
| 437 | + """Greedy NMM over a CSR match adjacency. Mirrors ``greedy_nmm_from_matrix``. |
| 438 | +
|
| 439 | + Args: |
| 440 | + indptr: CSR row pointers of length N + 1. |
| 441 | + indices: CSR column indices. |
| 442 | + sorted_idxs: Indices sorted by score descending. |
| 443 | +
|
| 444 | + Returns: |
| 445 | + Dict mapping each kept index to a list of indices merged into it. |
| 446 | + """ |
| 447 | + n = len(indptr) - 1 |
| 448 | + suppressed = np.zeros(n, dtype=bool) |
| 449 | + |
| 450 | + # The dense loop only considers candidates that come later in score order, |
| 451 | + # and emits them in that order. |
| 452 | + rank = np.empty(n, dtype=np.intp) |
| 453 | + rank[sorted_idxs] = np.arange(n) |
| 454 | + |
| 455 | + keep_to_merge_list: dict[int, list[int]] = {} |
| 456 | + for position, idx in enumerate(sorted_idxs): |
| 457 | + if suppressed[idx]: |
| 458 | + continue |
| 459 | + |
| 460 | + neighbours = indices[indptr[idx] : indptr[idx + 1]] |
| 461 | + merge_indices = neighbours[(rank[neighbours] > position) & ~suppressed[neighbours]] |
| 462 | + merge_indices = merge_indices[np.argsort(rank[merge_indices])] |
| 463 | + |
| 464 | + suppressed[merge_indices] = True |
| 465 | + keep_to_merge_list[int(idx)] = merge_indices.tolist() |
| 466 | + |
| 467 | + return keep_to_merge_list |
| 468 | + |
| 469 | + |
386 | 470 | def nmm_sparse( |
387 | 471 | indptr: np.ndarray, |
388 | 472 | indices: np.ndarray, |
|
0 commit comments