OpenCvSharpによる物流パッケージ上の既存ラベル衝突検知アルゴリズム

システム概略と検出メカニズム

この実装は、輸送用段ボールに既存のメーカーラベル(FBA マーケティングシール等)が存在する場合、新しい配送ラベルを適切に配置するための画像解析技術です。主に明度差(グレースケール値)を利用し、段ボールの素材色と印刷された白色ラベルの色調差分を検出することで、被覆箇所を特定します。

  • ROI 抽出: カメラ視野とボックスサイズが固定であることを前提とし、物理寸法からピクセル座標への変換を行います。
  • 閾値分析: 多数サンプルからの統計により、ラベル材特有の明るさ範囲を定義します。
  • 領域判定: ラベル存在エリア以外の空きグリッドを探索し、衝突しない座標を返却します。

解像度換算と初期化パラメータ

まず、物理サイズ(mm)と画像解像度(DPI)の比率を計算して、ピクセル単位の作業基準を確立します。

// DPI から mm あたりのピクセル密度を算出
// 例:300DPI の場合、約 11.81 ピクセル/mm
private const double Dpi = 300.0;
private static readonly double PixelsPerMillimeter = Dpi / 25.4;

// 目標ラベルサイズ(mm)をピクセル単位へ変換
private const int TargetLabelWidthMm = 100; // 縦横 100mm と仮定
private static readonly int LabelPixelSize = (int)Math.Ceiling(TargetLabelWidthMm * PixelsPerMillimeter);

// ラベル特有の灰度値候補リスト(白系統)
private static readonly string[] WhitePaletteHex = 
{ 
    "#FFFFFF", "#FCFCFC", "#FEFEFE", "#E2E2E0", 
    "#DEDEDC", "#E0E0DE", "#CCCCCA", "#B2B2B0" 
};

// ラベル輝度の最小・最大許容値
private static int MinLumThreshold;
private static int MaxLumThreshold;

画像ローディングと輝度レンジ演算

入力イメージを読み込み、事前収集されたホワイトパターンに基づいて二値化の閾値を設定する処理を行います。

public void Initialize(string imagePath)
{
    // グレースケールで画像ロード
    var srcMat = Cv2.ImRead(imagePath, ImreadModes.Grayscale);
    
    if (srcMat.Empty()) throw new FileNotFoundException("Invalid Image Path");

    DetermineLuminanceBoundaries();
    Console.WriteLine($"有効な輝度範囲: {MinLumThreshold} - {MaxLumThreshold}");
    
    ProcessAnalysis(srcMat);
    srcMat.Dispose();
}

private void DetermineLuminanceBoundaries()
{
    // 実際のコードでは Hex を Gray に変換して min/max を取得
    var grayValues = WhitePaletteHex.Select(Conversion.HexToGrayscale);
    MinLumThreshold = grayValues.Min();
    MaxLumThreshold = grayValues.Max();
}

物理サイズに基づく領域クリップ

カメラ設置高さとボックス配置が固定されているため、画像の右下端から箱の物理的範囲に合わせて画像を切り取ります。

private Mat ExtractBoxArea(Mat source, float widthMm, float heightMm)
{
    int imgW = source.Cols;
    int imgH = source.Rows;

    // mm から px へ変換
    int cropW = (int)(widthMm * PixelsPerMillimeter);
    int cropH = (int)(heightMm * PixelsPerMillimeter);

    // 切り取り開始点(右下寄せ)
    int startX = Math.Max(0, imgW - cropW);
    int startY = Math.Max(0, imgH - cropH);

    // ブードル外への逸脱防止
    cropW = Math.Min(cropW, imgW - startX);
    cropH = Math.Min(cropH, imgH - startY);

    if (cropW <= 0 || cropH <= 0) return source.Clone();

    Console.WriteLine($"トリミング実行: 原点({startX}, {startY}), サイズ({cropW}x{cropH})");
    return new Mat(source, new Rect(startX, startY, cropW, cropH));
}

既存ラベル位置の同定処理

切り出した画像に対して閾値処理を施し、ノイズ除去を行った後に輪郭を検出します。面積フィルタリングにより、微小な傷や汚れを除外し、ラベルとしての形状を抽出します。

public List<DetectedLabelInfo> ScanExistingLabels(Mat targetImg)
{
    var detectedLabels = new List<DetectedLabelInfo>();

    // 1. 明度ベースの二値化(逆二値化の場合あり、今回は白拉記物を抽出)
    var binImg = new Mat();
    Cv2.Threshold(targetImg, binImg, MinLumThreshold, 255, ThresholdTypes.Binary);

    // 2. 構造要素によるモルフォロジー処理(欠損埋め合わせとノイズ除去)
    var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5));
    Cv2.MorphologyEx(binImg, binImg, MorphTypes.Open, kernel);

    // 3. 輪郭検出
    Cv2.FindContours(binImg, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);

    foreach (var cnt in contours)
    {
        // 面積フィルタリング(最小サイズ以下は無視)
        if (Cv2.ContourArea(cnt) < 1500) continue;

        var boundingRect = Cv2.BoundingRect(cnt);
        
        detectedLabels.Add(new DetectedLabelInfo
        {
            Boundary = boundingRect,
            PositionName = ToGridNotation(boundingRect, targetImg.Rows, targetImg.Cols)
        });
    }
    return detectedLabels;
}

空きグリッドの探索ロジック

ラベルサイズの矩形で画像全体をグリッド分割し、既存ラベルのバウンディングボックスと重ならない、かつ画素値が均一なエリアを探します。

public List<string> QueryValidPlacementSlots(Mat workingImage, List<DetectedLabelInfo> foundLabels)
{
    var validSlots = new List<string>();
    int h = workingImage.Rows;
    int w = workingImage.Cols;

    int gridCountW = (int)Math.Ceiling((double)w / LabelPixelSize);
    int gridCountH = (int)Math.Ceiling((double)h / LabelPixelSize);

    for (int r = 1; r <= gridCountH; r++)
    {
        for (int c = 1; c <= gridCountW; c++)
        {
            // グリッド座標計算(右下を基準とする場合の調整が必要か確認)
            int rectX = w - c * LabelPixelSize;
            int rectY = h - r * LabelPixelSize;
            
            // 境界条件チェック
            if (rectX < 0) rectX = 0;
            if (rectY < 0) rectY = 0;

            Rect testArea = new Rect(rectX, rectY, LabelPixelSize, LabelPixelSize);

            // 既存ラベルとの当たり判定
            bool overlapFound = foundLabels.Any(lbl => lbl.Boundary.IntersectsWith(testArea));

            if (!overlapFound)
            {
                // 区域内部の濃淡変動を確認(段ボール自体の影などを避ける)
                if (CheckUniformTexture(workingImage, testArea))
                {
                    validSlots.Add($"{r}-{c}");
                }
            }
        }
    }
    
    return validSlots;
}

解析結果の可視化表示

最終的に、緑色の網掛けで全グリッド、赤で検出済みのラベル、青で推奨貼付エリアを描画します。

public Mat RenderVisualization(Mat src, List<DetectedLabelInfo> occupied, List<string> freeZones)
{
    // RGB 画像へ変換
    var resultImg = new Mat();
    Cv2.CvtColor(src, resultImg, ColorConversionCodes.GRAY2BGR);

    int rows = src.Rows;
    int cols = src.Cols;

    // グリッドライン描画(Green)
    for (int x = 0; x < cols; x += LabelPixelSize)
        Cv2.Line(resultImg, new Point(x, 0), new Point(x, rows), Scalar.Green, 1);
    for (int y = 0; y < rows; y += LabelPixelSize)
        Cv2.Line(resultImg, new Point(0, y), new Point(cols, y), Scalar.Green, 1);

    // 既在ラベル表示(Red)
    foreach (var lbl in occupied)
    {
        Cv2.Rectangle(resultImg, lbl.Boundary, Scalar.Red, 2);
        Cv2.PutText(resultImg, lbl.PositionName, 
            new Point(lbl.Boundary.X, lbl.Boundary.Y - 5), 
            HersheyFonts.HersheyDuplex, 0.6, Scalar.Red, 1);
    }

    // 推奨位置表示(Blue)
    foreach (var zone in freeZones)
    {
        // パースして座標復元
        var coords = zone.Split('-');
        int r = int.Parse(coords[0]);
        int c = int.Parse(coords[1]);
        
        int cx = Math.Max(0, cols - c * LabelPixelSize);
        int cy = Math.Max(0, rows - r * LabelPixelSize);
        
        Cv2.Rectangle(resultImg, 
            new Point(cx, cy), 
            new Point(cx + LabelPixelSize, cy + LabelPixelSize), 
            Scalar.Blue, 2);
            
        Cv2.PutText(resultImg, zone, 
            new Point(cx + 5, cy + 20), 
            HersheyFonts.HersheyComplexSmall, 0.4, Scalar.Blue, 1);
    }

    return resultImg;
}

タグ: OpenCvSharp C# ComputerVision ImageProcessing LogisticsAutomation

10月15日 20:23 投稿