study

相較於 NMS 只保留最高分,其餘刪除的作法,Weighted Box Fusion (WBF) 採用的是融合策略。它利用所有重疊框的資訊,計算出加權平均後的座標,從而產生一個更精準的邊界框。這在多模型集成,或有高度重疊物件的場景中特別有效。

演算法

WBF 將重疊度高於閾值的框歸為同一群,並利用置信度作為權重來修正最終座標。

融合公式

對於一個聚類中的 個框,最終座標 計算如下(置信度越高,話語權越重):

應用

Kibo RPC 2025 中,前期受限於資料量不足,單一模型準確度不高,我們採用了模型集成策略,訓練了多個針對不同尺度與情境的模型,然後用 WBF 進行結果融合。後期單一模型的性能已顯著提升且足以應對多數情況,但仍保留使用 WBF。

改良版演算法

改良版 vs. 傳統

相較於標準 WBF,此修正版針對極端尺度差異與雜訊過濾進行了以下改良:

  • 匹配機制
    • 傳統:僅依賴 IoU > Threshold 來判斷重疊
    • 改良版:新增 OR Containment > Threshold 判斷條件
    • 目的:解決「大框完全包覆小框」時,因 IoU 分母過大導致數值過低,而無法正確融合的問題
  • 雜訊過濾
    • 傳統:對融合後的結果照單全收,不做檢查
    • 改良版:新增幾何過濾機制(若 WidthHeight < 5% 則捨棄)
    • 目的:剔除在融合過程中可能產生的異常扁平或細長的無效雜訊
  • 權重計算
    • 傳統:僅使用預測框的信心作為權重
    • 改良版:引入模型權重 ()
    • 目的:允許開發者針對不同模型設定權重(例如:給予近距離、高解析度的模型更高話語權),讓可靠的模型主導結果
  • 類別決策
    • 傳統:通常直接沿用第一個框的類別,或對類別分數做平均
    • 改良版:融合座標後,對聚類進行重排序,並選取加權分數最高者的類別
    • 目的:在多類別容易混淆的場景下,確保由信心度最強的預測來源來決定最終標籤,避免多數決導致的平庸錯誤

程式碼

  /**
   * Applies Revised Weighted Box Fusion (WBF) to combine overlapping detections.
   * This method is a modified version of the original WBF algorithm.
   * Instead of merging boxes by class, it merges all boxes of different classes first,
   * then chooses the best class based on the weighted average of the confidence scores.
   *
   * @param detections List of Detection objects to be fused.
   * @param iouThreshold IoU threshold for merging boxes.
   * @param confThreshold Confidence threshold for filtering boxes.
   * @return List of fused Detection objects.
   */
  private List<Detection> wbf(List<Detection> detections, float iouThreshold, float containmentThreshould, float confThreshold) {
    if (detections.isEmpty()) {
      Log.i(TAG, "No detections to process.");
      return detections;
    }
 
    // Sort the score in descending order
    Collections.sort(detections, new Comparator<Detection>() {
      @Override
      public int compare(Detection d1, Detection d2) {
        return Float.compare(d2.confidence, d1.confidence);
      }
    });
 
    List<Detection> fused = new ArrayList<>();
    boolean[] used = new boolean[detections.size()];
 
    // Iterate through the detections
    for (int i = 0; i < detections.size(); i++) {
      if (used[i]) continue;
 
      // Create a new group for the current detection
      List<Detection> group = new ArrayList<>();
      group.add(detections.get(i));
      used[i] = true;
 
      // Check for overlapping detections
      for (int j = i + 1; j < detections.size(); j++) {
        if (used[j]) continue;
 
        Detection di = detections.get(i);
        Detection dj = detections.get(j);
        float iou = calculateIoU(di.box, dj.box);
        float containment = calculateContainment(dj.box, di.box);
 
        if (iou > iouThreshold || containment > containmentThreshould) {
          group.add(dj);
          used[j] = true;
        }
      }
 
      // Compute weighted box
      float confidenceSum = 0f;
      float weightSum = 0f;
      float x1 = 0f, y1 = 0f, x2 = 0f, y2 = 0f;
 
      for (Detection d : group) {
        confidenceSum += d.confidence * d.modelWeight;
        weightSum += d.modelWeight;
        x1 += d.box[0] * d.confidence * d.modelWeight;
        y1 += d.box[1] * d.confidence * d.modelWeight;
        x2 += d.box[2] * d.confidence * d.modelWeight;
        y2 += d.box[3] * d.confidence * d.modelWeight;
      }
 
      float confidenceAvg = confidenceSum / weightSum;
      if (confidenceAvg < confThreshold) {
        continue;
      }
 
      x1 /= confidenceSum;
      y1 /= confidenceSum;
      x2 /= confidenceSum;
      y2 /= confidenceSum;
 
      float minX = Math.min(x1, x2);
      float minY = Math.min(y1, y2);
      float maxX = Math.max(x1, x2);
      float maxY = Math.max(y1, y2);
 
      // Check if the bounding box is too small
      if ((maxX - minX) < 0.05 || (maxY - minY) < 0.05) {
        continue;
      }
 
      // Sort into descending order of confidence
      Collections.sort(group, new Comparator<Detection>() {
        @Override
        public int compare(Detection a, Detection b) {
          float weightedConfidenceA = a.confidence * a.modelWeight;
          float weightedConfidenceB = b.confidence * b.modelWeight;
          return Float.compare(weightedConfidenceB, weightedConfidenceA);
        }
      });
 
      Detection fusedDetection = new Detection(
        new float[]{minX, minY, maxX, maxY},
        confidenceAvg,
        group.get(0).classId,
        group.get(0).className
      );
      fused.add(fusedDetection);
    }
 
    return fused;
  }

IoU 的盲點

注意到由於 IoU 計算的是交集與聯集的比例,當小框完全被大框包含,但面積差異巨大時,IoU 數值會因為分母過大而偏低,導致無法正確識別這種包含關係。

  • IoU 公式定義
  • 分子 (交集):兩個框重疊的部分

  • 分母 (聯集):兩個框加起來的總面積(扣除重疊部分)

  • 問題情境

    • 當一個極小的框完全位於一個極大的框裡面時
    • 交集:小框的面積 ()
    • 聯集:大框的面積 ()
    • 計算結果
    • 這種數值偏差會讓演算法(如 NMS 或 WBF)產生誤解:這兩個框的 IoU 只有 0.04 ,遠低於閾值,這是兩個完全不同的東西,保留它們!
    • 實際上,這兩個框指的是同一個物體,只是其中一個預測得比較大,另一個比較精細。若只依賴 IoU,會導致無法正確融合或移除這些重疊框。

包含率 Containment

為了修復 IoU 在大框包小框時的數值缺陷,我們引入了 Containment 指標。

  • 公式定義
  • 分子 (交集):與 IoU 相同,為兩個框重疊的部分

  • 分母 (最小面積):這是關鍵差異。我們不再除以巨大的聯集,而是除以較小那個框的面積

  • 回到剛才的例子

    • 小框 () 完全位於大框 () 內部
    • 交集:依然是小框的面積 ()
    • 分母:取兩者中的最小值,即小框面積 ()
    • 計算結果
  • 在實作中採用 OR 邏輯來結合兩者

    • 只要 IoU > T_iou 或者 Containment > T_cont 其中一項成立,就視為重疊
    • 同時兼顧一般重疊與尺寸懸殊的包含關係