检测 hull 图像中主体区域相对其凸包产生的凹陷/缺口区域。

ysj 103
Viewed 37

检出红色圈线的区域hull.png
image.png

2 Answers

使用lpvsdk实现流程如下:


/**
 * @file Hull.cpp
 * @brief 船体轮廓凹陷区域测量案例
 *
 * 逻辑:
 *   1. 读取 hull 图像并转换为 8 位灰度图。
 *   2. 通过阈值、差集、连通域、面积过滤和形态学得到主体区域。
 *   3. 对主体区域构造凸包掩码,再与主体掩码做差得到凹陷候选。
 *   4. 按面积和凸性过滤最终区域,输出面积与中心点到 JSON。
 */

#include "CaseUtils.h"
#include "LPVBlob.h"
#include "LPVImgProc.h"

#include <algorithm>

using namespace LPVBlobLib;
using namespace LPVImgProcLib;

int main()
{
    CoInitialize(nullptr);

    const std::string outputPath = resultOutputDir() + "/Hull.json";

    ILImagePtr image = LImage::Create();
    const std::string imagePath = findImageByName("hull");
    if (imagePath.empty())
        throw std::runtime_error("failed to locate image: hull");
    LPVErrorCode err = image->Load(toWide(imagePath).c_str());
    if (err != LPVErrorCode::LPVNoError)
        throw std::runtime_error("failed to load image: hull");

    // 将输入统一为灰度图,保证固定阈值在同一灰度空间执行。
    ILImageConvertPtr convert = LImageConvert::Create();
    ILImagePtr grayImage = image;
    if (image->ImageFormat != LPVImageFormatGrayscale8) {
        grayImage = LImage::Create();
        convert->BGRToGray(image, grayImage);
    }

    const int width = grayImage->Width;
    const int height = grayImage->Height;
    std::vector<unsigned char> whiteData(static_cast<size_t>(width) * static_cast<size_t>(height), 255);
    std::vector<unsigned char> blackData(static_cast<size_t>(width) * static_cast<size_t>(height), 0);

    // 全图掩码表示完整图像域,后续差集在二值图上完成。
    ILImagePtr fullMask = LImage::Create();
    fullMask->ImageFormat = LPVImageFormatGrayscale8;
    fullMask->SetImageData(width, height, whiteData.data(), width, true);

    // 固定低灰度阈值提取暗区域。
    ILImageThresholdPtr threshold = LImageThreshold::Create();
    threshold->SetThreshold(0, 80);
    ILImagePtr darkMask = LImage::Create();
    threshold->Binarize(grayImage, darkMask);

    ILImageOpPtr imageOp = LImageOp::Create();

    // 从完整图像域中扣除暗区域,得到亮色主体候选掩码。
    ILImagePtr invertedDarkMask = LImage::Create();
    imageOp->Invert(darkMask, invertedDarkMask);
    ILImagePtr lightMask = LImage::Create();
    imageOp->BitAnd(fullMask, invertedDarkMask, lightMask);

    // 对亮色主体候选做连通域分析并保留大面积区域。
    ILBlobAnalysisPtr lightAnalysis = LBlobAnalysis::Create();
    lightAnalysis->ColorMode = LPVImageFormatGrayscale8;
    lightAnalysis->FillHole = false;
    lightAnalysis->AddBlobRange(255, 255);
    lightAnalysis->MaxCount = 10000;
    lightAnalysis->ContourType = LPVBlobContourType::LPVBlobContourExternal;
    lightAnalysis->Hierarchy = 0;
    ILBlobResultsPtr lightBlobs;
    lightAnalysis->Build(lightMask, ILRegionPtr(), &lightBlobs);

    ILBlobFilterPtr noHullAreaFilter = LBlobFilter::Create();
    noHullAreaFilter->SetFilterFeature(LPVBlobFeatures::LPVBlobArea, 50000.0, 9999999.0);
    ILBlobResultsPtr noHullCandidates = lightBlobs ? noHullAreaFilter->FilterResults(lightBlobs) : ILBlobResultsPtr();

    // 将筛选出的主体候选光栅化为掩码,供圆形闭运算使用。
    ILImagePtr noHullCandidateMask = LImage::Create();
    noHullCandidateMask->ImageFormat = LPVImageFormatGrayscale8;
    noHullCandidateMask->SetImageData(width, height, blackData.data(), width, true);
    if (noHullCandidates) {
        for (int i = 0; i < noHullCandidates->Count(); ++i) {
            ILBlobPtr blob = noHullCandidates->Item(i);
            if (!blob)
                continue;
            ILMaskRegionPtr region = blob->ToRegion();
            if (!region)
                continue;
            ILImagePtr mask = region->ToMask(0, 0, width, height);
            if (!mask || mask->Width <= 0 || mask->Height <= 0)
                continue;
            ILImagePtr merged = LImage::Create();
            imageOp->BitOr(noHullCandidateMask, mask, merged);
            noHullCandidateMask = merged;
        }
    }

    // 椭圆结构元闭运算填补主体候选的小缺口。
    ILImageMorphPtr closeMorph = LImageMorph::Create();
    closeMorph->SetMorphShape(LPVMorphEllipse, 27, 27);
    ILImagePtr noHullMask = LImage::Create();
    closeMorph->Close(noHullCandidateMask, noHullMask);

    // 从完整图像域扣除闭运算后的主体,获得补集候选。
    ILImagePtr invertedNoHullMask = LImage::Create();
    imageOp->Invert(noHullMask, invertedNoHullMask);
    ILImagePtr regionMask = LImage::Create();
    imageOp->BitAnd(fullMask, invertedNoHullMask, regionMask);

    // 椭圆结构元开运算清理补集候选中的小噪声。
    ILImageMorphPtr openMorph = LImageMorph::Create();
    openMorph->SetMorphShape(LPVMorphEllipse, 5, 5);
    ILImagePtr regionOpeningMask = LImage::Create();
    openMorph->Open(regionMask, regionOpeningMask);

    // 对清理后的候选做连通域分析并保留大面积主体区域。
    ILBlobAnalysisPtr regionAnalysis = LBlobAnalysis::Create();
    regionAnalysis->ColorMode = LPVImageFormatGrayscale8;
    regionAnalysis->FillHole = false;
    regionAnalysis->AddBlobRange(255, 255);
    regionAnalysis->MaxCount = 10000;
    regionAnalysis->ContourType = LPVBlobContourType::LPVBlobContourExternal;
    regionAnalysis->Hierarchy = 0;
    ILBlobResultsPtr regionBlobs;
    regionAnalysis->Build(regionOpeningMask, ILRegionPtr(), &regionBlobs);

    ILBlobFilterPtr regionAreaFilter = LBlobFilter::Create();
    regionAreaFilter->SetFilterFeature(LPVBlobFeatures::LPVBlobArea, 5000.0, 9999999.0);
    ILBlobResultsPtr regionHullBlobs = regionBlobs ? regionAreaFilter->FilterResults(regionBlobs) : ILBlobResultsPtr();

    // 光栅化筛选后的主体区域,作为凸包差集的被扣除掩码。
    ILImagePtr regionHullMask = LImage::Create();
    regionHullMask->ImageFormat = LPVImageFormatGrayscale8;
    regionHullMask->SetImageData(width, height, blackData.data(), width, true);
    if (regionHullBlobs) {
        for (int i = 0; i < regionHullBlobs->Count(); ++i) {
            ILBlobPtr blob = regionHullBlobs->Item(i);
            if (!blob)
                continue;
            ILMaskRegionPtr region = blob->ToRegion();
            if (!region)
                continue;
            ILImagePtr mask = region->ToMask(0, 0, width, height);
            if (!mask || mask->Width <= 0 || mask->Height <= 0)
                continue;
            ILImagePtr merged = LImage::Create();
            imageOp->BitOr(regionHullMask, mask, merged);
            regionHullMask = merged;
        }
    }

    // 对主体区域构造凸包并光栅化为统一掩码。
    ILImagePtr convexHullMask = LImage::Create();
    convexHullMask->ImageFormat = LPVImageFormatGrayscale8;
    convexHullMask->SetImageData(width, height, blackData.data(), width, true);
    if (regionHullBlobs) {
        for (int i = 0; i < regionHullBlobs->Count(); ++i) {
            ILBlobPtr blob = regionHullBlobs->Item(i);
            if (!blob)
                continue;
            ILPolygonPtr hull = blob->GetConvexHull();
            if (!hull)
                continue;
            ILPolyRegionPtr hullRegion = hull->ToPolyRegion();
            if (!hullRegion)
                continue;
            ILImagePtr mask = hullRegion->ToMask(0, 0, width, height);
            if (!mask || mask->Width <= 0 || mask->Height <= 0)
                continue;
            ILImagePtr merged = LImage::Create();
            imageOp->BitOr(convexHullMask, mask, merged);
            convexHullMask = merged;
        }
    }

    // 凸包掩码扣除主体掩码,得到凹陷候选区域。
    ILImagePtr invertedRegionHullMask = LImage::Create();
    imageOp->Invert(regionHullMask, invertedRegionHullMask);
    ILImagePtr deviationMask = LImage::Create();
    imageOp->BitAnd(convexHullMask, invertedRegionHullMask, deviationMask);

    // 对凹陷候选做连通域、面积和凸性过滤。
    ILBlobAnalysisPtr deviationAnalysis = LBlobAnalysis::Create();
    deviationAnalysis->ColorMode = LPVImageFormatGrayscale8;
    deviationAnalysis->FillHole = false;
    deviationAnalysis->AddBlobRange(255, 255);
    deviationAnalysis->MaxCount = 10000;
    deviationAnalysis->ContourType = LPVBlobContourType::LPVBlobContourExternal;
    deviationAnalysis->Hierarchy = 0;
    ILBlobResultsPtr deviationBlobs;
    deviationAnalysis->Build(deviationMask, ILRegionPtr(), &deviationBlobs);

    ILBlobFilterPtr largeHoleFilter = LBlobFilter::Create();
    largeHoleFilter->SetFilterFeature(LPVBlobFeatures::LPVBlobArea, 2000.0, 99999.0);
    ILBlobResultsPtr largeHoles = deviationBlobs ? largeHoleFilter->FilterResults(deviationBlobs) : ILBlobResultsPtr();

    ILBlobFilterPtr convexityFilter = LBlobFilter::Create();
    convexityFilter->SetFilterFeature(LPVBlobFeatures::LPVBlobConvexity, 0.0, 0.85);
    ILBlobResultsPtr holes = largeHoles ? convexityFilter->FilterResults(largeHoles) : ILBlobResultsPtr();

    struct HoleMeasurement {
        double area = 0.0;
        double row = 0.0;
        double column = 0.0;
    };

    std::vector<HoleMeasurement> measurements;
    const int holeCount = holes ? holes->Count() : 0;
    measurements.reserve(static_cast<size_t>(holeCount));
    for (int i = 0; i < holeCount; ++i) {
        ILBlobPtr blob = holes->Item(i);
        if (!blob)
            continue;
        HoleMeasurement item;
        item.area = blob->GetFeature(LPVBlobFeatures::LPVBlobArea);
        item.row = blob->GetFeature(LPVBlobFeatures::LPVBlobCenterY);
        item.column = blob->GetFeature(LPVBlobFeatures::LPVBlobCenterX);
        measurements.push_back(item);
    }

    for (size_t i = 0; i < measurements.size(); ++i) {
        for (size_t j = i + 1; j < measurements.size(); ++j) {
            const bool shouldSwap = (measurements[j].row < measurements[i].row) ||
                (measurements[j].row == measurements[i].row && measurements[j].column < measurements[i].column);
            if (shouldSwap)
                std::swap(measurements[i], measurements[j]);
        }
    }

    std::vector<std::string> holeEntries;
    holeEntries.reserve(measurements.size());
    for (size_t i = 0; i < measurements.size(); ++i) {
        const HoleMeasurement& hole = measurements[i];
        std::string entry;
        entry += "    {\n";
        entry += "      \"id\": " + std::to_string(static_cast<int>(i) + 1) + ",\n";
        entry += "      \"area\": " + formatDouble(hole.area, 3) + ",\n";
        entry += "      \"row\": " + formatDouble(hole.row, 3) + ",\n";
        entry += "      \"column\": " + formatDouble(hole.column, 3) + "\n";
        entry += "    }";
        holeEntries.push_back(entry);
    }

    std::string jsonText;
    jsonText += "{\n";
    jsonText += "  \"case_name\": \"hull\",\n";
    jsonText += "  \"image_name\": \"hull\",\n";
    jsonText += "  \"hole_count\": " + std::to_string(static_cast<int>(measurements.size())) + ",\n";
    jsonText += "  \"holes\": [\n";
    if (!holeEntries.empty())
        jsonText += join(holeEntries, ",\n") + "\n";
    jsonText += "  ]\n";
    jsonText += "}\n";

    writeTextFile(outputPath, jsonText);

    CoUninitialize();
    return 0;
}

方案.png结果.png

方案如图,鲁棒性待验证。
小建议:相机的角度和打光可以优化