Replace ObjectMask EdgeSAM seed masks with EfficientSAM ONNX

- Use a single EfficientSAM ONNX model for ObjectMask seed mask generation
- Add EfficientSAM prompt preprocessing and mask candidate selection
- Keep seed-frame output as the raw EfficientSAM mask while still seeding XMem
- Add ClipProcessingJobs::PreviewObjectMask for single-frame interactive previews
- Accept EfficientSAM model JSON keys while preserving legacy encoder_model aliases
- Remove ObjectMask protobuf shutdown call that could destabilize Python teardown
- Add ObjectMask ONNX validation coverage
This commit is contained in:
Jonathan Thomas
2026-05-18 19:18:56 -05:00
parent 1f0b1b9698
commit 8bd98a592b
5 changed files with 207 additions and 120 deletions
+177 -111
View File
@@ -69,39 +69,35 @@ std::vector<uint32_t> EncodeBinaryMaskRLE(const cv::Mat& mask)
return rle;
}
struct SamPreprocessResult {
struct EfficientSamPreprocessResult {
cv::Mat blob;
float scale = 1.0f;
int resizedWidth = 0;
int resizedHeight = 0;
float scaleX = 1.0f;
float scaleY = 1.0f;
};
SamPreprocessResult MakeSamBlob(const cv::Mat& bgr, int modelSize)
EfficientSamPreprocessResult MakeEfficientSamBlob(const cv::Mat& bgr, int modelSize)
{
SamPreprocessResult result;
result.scale = static_cast<float>(modelSize) / static_cast<float>(std::max(bgr.cols, bgr.rows));
result.resizedWidth = static_cast<int>(bgr.cols * result.scale + 0.5f);
result.resizedHeight = static_cast<int>(bgr.rows * result.scale + 0.5f);
EfficientSamPreprocessResult result;
result.scaleX = static_cast<float>(modelSize) / static_cast<float>(bgr.cols);
result.scaleY = static_cast<float>(modelSize) / static_cast<float>(bgr.rows);
cv::Mat resized;
cv::resize(bgr, resized, cv::Size(result.resizedWidth, result.resizedHeight), 0, 0, cv::INTER_LINEAR);
cv::resize(bgr, resized, cv::Size(modelSize, modelSize), 0, 0, cv::INTER_LINEAR);
const int shape[] = {1, 3, modelSize, modelSize};
result.blob = cv::Mat(4, shape, CV_32F, cv::Scalar(0.0f));
result.blob = cv::Mat(4, shape, CV_32F);
float* dst = result.blob.ptr<float>();
const float mean[] = {123.675f, 116.28f, 103.53f};
const float stddev[] = {58.395f, 57.12f, 57.375f};
for (int y = 0; y < resized.rows; ++y) {
const cv::Vec3b* row = resized.ptr<cv::Vec3b>(y);
for (int x = 0; x < resized.cols; ++x) {
const float rgb[] = {
static_cast<float>(row[x][2]),
static_cast<float>(row[x][1]),
static_cast<float>(row[x][0]),
static_cast<float>(row[x][2]) / 255.0f,
static_cast<float>(row[x][1]) / 255.0f,
static_cast<float>(row[x][0]) / 255.0f,
};
for (int c = 0; c < 3; ++c)
dst[(c * modelSize + y) * modelSize + x] = (rgb[c] - mean[c]) / stddev[c];
dst[(c * modelSize + y) * modelSize + x] = rgb[c];
}
}
@@ -123,15 +119,10 @@ cv::Rect_<float> NormalizedBoundingBox(const cv::Mat& mask)
rect.height / static_cast<float>(mask.rows));
}
cv::Mat LowMaskToFrameMask(const cv::Mat& lowMask, const SamPreprocessResult& prep,
const cv::Size& frameSize, int modelSize, float maskThreshold)
cv::Mat EfficientSamMaskToFrameMask(const cv::Mat& modelMask, const cv::Size& frameSize, float maskThreshold)
{
cv::Mat paddedMask;
cv::resize(lowMask, paddedMask, cv::Size(modelSize, modelSize), 0, 0, cv::INTER_LINEAR);
cv::Mat cropped = paddedMask(cv::Rect(0, 0, prep.resizedWidth, prep.resizedHeight));
cv::Mat fullSize;
cv::resize(cropped, fullSize, frameSize, 0, 0, cv::INTER_LINEAR);
cv::resize(modelMask, fullSize, frameSize, 0, 0, cv::INTER_LINEAR);
cv::Mat binary;
cv::threshold(fullSize, binary, maskThreshold, 255.0, cv::THRESH_BINARY);
@@ -139,6 +130,99 @@ cv::Mat LowMaskToFrameMask(const cv::Mat& lowMask, const SamPreprocessResult& pr
return binary;
}
cv::Mat MakeEfficientSamPromptBlob(
const CVObjectMaskPromptSet& prompts,
const EfficientSamPreprocessResult& prep,
int promptSlots,
std::vector<cv::Point>& backgroundPoints)
{
const int coordsShape[] = {1, 1, promptSlots, 2};
cv::Mat pointCoords(4, coordsShape, CV_32F, cv::Scalar(0.0f));
float* coords = pointCoords.ptr<float>();
int promptIndex = 0;
if (prompts.hasRect && promptSlots >= 2) {
coords[0] = prompts.rectTopLeft.x * prep.scaleX;
coords[1] = prompts.rectTopLeft.y * prep.scaleY;
coords[2] = prompts.rectBottomRight.x * prep.scaleX;
coords[3] = prompts.rectBottomRight.y * prep.scaleY;
promptIndex = 2;
}
for (const auto& point : prompts.positivePoints) {
if (promptIndex >= promptSlots)
break;
coords[promptIndex * 2] = point.x * prep.scaleX;
coords[promptIndex * 2 + 1] = point.y * prep.scaleY;
++promptIndex;
}
for (const auto& point : prompts.negativePoints) {
backgroundPoints.emplace_back(
static_cast<int>(std::lround(point.x * prep.scaleX)),
static_cast<int>(std::lround(point.y * prep.scaleY)));
}
return pointCoords;
}
cv::Mat MakeEfficientSamLabelBlob(const CVObjectMaskPromptSet& prompts, int promptSlots)
{
const int labelsShape[] = {1, 1, promptSlots, 1};
cv::Mat pointLabels(4, labelsShape, CV_32F, cv::Scalar(-1.0f));
float* labels = pointLabels.ptr<float>();
int promptIndex = 0;
if (prompts.hasRect && promptSlots >= 2) {
labels[0] = 2.0f;
labels[1] = 3.0f;
promptIndex = 2;
}
for (size_t i = 0; i < prompts.positivePoints.size() && promptIndex < promptSlots; ++i, ++promptIndex)
labels[promptIndex] = 1.0f;
return pointLabels;
}
cv::Mat SelectEfficientSamMask(const cv::Mat& outputMasks, const cv::Mat& iouPredictions,
const std::vector<cv::Point>& backgroundPoints, float maskThreshold)
{
if (outputMasks.dims != 5 || iouPredictions.empty())
return cv::Mat();
const int candidateCount = outputMasks.size[2];
const int maskHeight = outputMasks.size[3];
const int maskWidth = outputMasks.size[4];
const float* ious = iouPredictions.ptr<float>();
std::vector<int> order(candidateCount);
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&](int a, int b) {
return ious[a] > ious[b];
});
const float* masks = outputMasks.ptr<float>();
const size_t candidatePixels = static_cast<size_t>(maskHeight) * static_cast<size_t>(maskWidth);
cv::Mat fallback;
for (int candidate : order) {
cv::Mat mask(maskHeight, maskWidth, CV_32F,
const_cast<float*>(masks + static_cast<size_t>(candidate) * candidatePixels));
if (fallback.empty())
fallback = mask.clone();
bool containsBackground = false;
for (const cv::Point& point : backgroundPoints) {
const int x = std::max(0, std::min(maskWidth - 1, point.x));
const int y = std::max(0, std::min(maskHeight - 1, point.y));
if (mask.at<float>(y, x) >= maskThreshold) {
containsBackground = true;
break;
}
}
if (!containsBackground)
return mask.clone();
}
return fallback;
}
CVObjectMaskFrameData FrameDataFromMask(const cv::Mat& mask, size_t frameId, float score)
{
CVObjectMaskFrameData frameData;
@@ -566,12 +650,40 @@ CVObjectMask::CVObjectMask(std::string processInfoJson, ProcessingController& co
SetJson(processInfoJson);
}
std::string CVObjectMask::ValidateONNXModels(std::string encoderPath, std::string decoderPath)
std::string CVObjectMask::ValidateONNXModel(std::string modelPath)
{
std::string error = LoadONNXModel(encoderPath, nullptr);
if (!error.empty())
return error;
return LoadONNXModel(decoderPath, nullptr);
return LoadONNXModel(modelPath, nullptr);
}
std::shared_ptr<Frame> CVObjectMask::PreviewSeedMask(std::shared_ptr<Frame> frame)
{
if (!frame || efficientSamModelPath.empty() || promptKeyframes.empty())
return std::shared_ptr<Frame>();
std::string loadError = LoadONNXModel(efficientSamModelPath, &efficientSam);
if (!loadError.empty())
return std::shared_ptr<Frame>();
SetProcessingDevice();
CVObjectMaskPromptSet prompts = promptKeyframes.begin()->second;
cv::Mat frameImage = frame->GetImageCV();
cv::Mat seedMask = CreateEfficientSAMSeedMask(frameImage, prompts);
if (seedMask.empty())
return std::shared_ptr<Frame>();
auto maskImage = std::make_shared<QImage>(
seedMask.cols, seedMask.rows, QImage::Format_RGBA8888_Premultiplied);
maskImage->fill(Qt::transparent);
for (int y = 0; y < seedMask.rows; ++y) {
const uint8_t* src = seedMask.ptr<uint8_t>(y);
QRgb* dst = reinterpret_cast<QRgb*>(maskImage->scanLine(y));
for (int x = 0; x < seedMask.cols; ++x)
dst[x] = src[x] ? qRgba(255, 255, 255, 255) : qRgba(0, 0, 0, 0);
}
auto result = std::make_shared<Frame>(frame->number, seedMask.cols, seedMask.rows, "#000000");
result->AddImage(maskImage);
return result;
}
void CVObjectMask::SetProcessingDevice()
@@ -580,10 +692,8 @@ void CVObjectMask::SetProcessingDevice()
try {
const std::vector<cv::dnn::Target> targets = cv::dnn::getAvailableTargets(cv::dnn::DNN_BACKEND_CUDA);
if (std::find(targets.begin(), targets.end(), cv::dnn::DNN_TARGET_CUDA) != targets.end()) {
encoder.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
encoder.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
decoder.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
decoder.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
efficientSam.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
efficientSam.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
return;
}
} catch (const cv::Exception&) {
@@ -591,10 +701,8 @@ void CVObjectMask::SetProcessingDevice()
processingDevice = "CPU";
}
encoder.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
encoder.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
decoder.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
decoder.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
efficientSam.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
efficientSam.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);
}
void CVObjectMask::maskClip(openshot::Clip& video, size_t _start, size_t _end, bool process_interval)
@@ -605,8 +713,8 @@ void CVObjectMask::maskClip(openshot::Clip& video, size_t _start, size_t _end, b
video.Open();
processingController->SetError(false, "");
if (encoderModelPath.empty() || decoderModelPath.empty()) {
processingController->SetError(true, "Missing path to EdgeSAM encoder or decoder ONNX model file");
if (efficientSamModelPath.empty()) {
processingController->SetError(true, "Missing path to EfficientSAM ONNX model file");
error = true;
return;
}
@@ -621,13 +729,7 @@ void CVObjectMask::maskClip(openshot::Clip& video, size_t _start, size_t _end, b
return;
}
std::string loadError = LoadONNXModel(encoderModelPath, &encoder);
if (!loadError.empty()) {
processingController->SetError(true, loadError);
error = true;
return;
}
loadError = LoadONNXModel(decoderModelPath, &decoder);
std::string loadError = LoadONNXModel(efficientSamModelPath, &efficientSam);
if (!loadError.empty()) {
processingController->SetError(true, loadError);
error = true;
@@ -705,7 +807,7 @@ void CVObjectMask::maskClip(openshot::Clip& video, size_t _start, size_t _end, b
const cv::Mat frameImage = frame->GetImageCV();
cv::Mat seedMask;
if (isPromptKeyframe || !xmem.HasMemory()) {
seedMask = CreateEdgeSAMSeedMask(frameImage, activePrompts);
seedMask = CreateEfficientSAMSeedMask(frameImage, activePrompts);
if (seedMask.empty()) {
CVObjectMaskFrameData emptyFrame;
emptyFrame.frameId = frameNumber;
@@ -726,8 +828,11 @@ void CVObjectMask::maskClip(openshot::Clip& video, size_t _start, size_t _end, b
}
cv::Mat outputMask;
if (!propagatedMask.empty())
if (!seedMask.empty()) {
outputMask = seedMask;
} else if (!propagatedMask.empty()) {
cv::resize(propagatedMask, outputMask, frameImage.size(), 0, 0, cv::INTER_NEAREST);
}
masksData[frameNumber] = FrameDataFromMask(outputMask, frameNumber, 1.0f);
const size_t range = std::max<size_t>(1, end - start);
@@ -735,65 +840,26 @@ void CVObjectMask::maskClip(openshot::Clip& video, size_t _start, size_t _end, b
}
}
cv::Mat CVObjectMask::CreateEdgeSAMSeedMask(const cv::Mat& frame, const CVObjectMaskPromptSet& prompts)
cv::Mat CVObjectMask::CreateEfficientSAMSeedMask(const cv::Mat& frame, const CVObjectMaskPromptSet& prompts)
{
SamPreprocessResult prep = MakeSamBlob(frame, modelSize);
encoder.setInput(prep.blob, "image");
cv::Mat embeddings = encoder.forward("image_embeddings");
EfficientSamPreprocessResult prep = MakeEfficientSamBlob(frame, modelSize);
std::vector<cv::Point> backgroundPoints;
cv::Mat pointCoords = MakeEfficientSamPromptBlob(prompts, prep, promptSlots, backgroundPoints);
cv::Mat pointLabels = MakeEfficientSamLabelBlob(prompts, promptSlots);
const int coordsShape[] = {1, promptSlots, 2};
const int labelsShape[] = {1, promptSlots};
cv::Mat pointCoords(3, coordsShape, CV_32F, cv::Scalar(0.0f));
cv::Mat pointLabels(2, labelsShape, CV_32F, cv::Scalar(-1.0f));
int promptIndex = 0;
if (prompts.hasRect && promptSlots >= 2) {
float* coords = pointCoords.ptr<float>();
float* labels = pointLabels.ptr<float>();
coords[0] = prompts.rectTopLeft.x * prep.scale;
coords[1] = prompts.rectTopLeft.y * prep.scale;
labels[0] = 2.0f;
coords[2] = prompts.rectBottomRight.x * prep.scale;
coords[3] = prompts.rectBottomRight.y * prep.scale;
labels[1] = 3.0f;
promptIndex = 2;
}
for (const auto& point : prompts.positivePoints) {
if (promptIndex >= promptSlots)
break;
pointCoords.ptr<float>()[promptIndex * 2] = point.x * prep.scale;
pointCoords.ptr<float>()[promptIndex * 2 + 1] = point.y * prep.scale;
pointLabels.ptr<float>()[promptIndex] = 1.0f;
++promptIndex;
}
for (const auto& point : prompts.negativePoints) {
if (promptIndex >= promptSlots)
break;
pointCoords.ptr<float>()[promptIndex * 2] = point.x * prep.scale;
pointCoords.ptr<float>()[promptIndex * 2 + 1] = point.y * prep.scale;
pointLabels.ptr<float>()[promptIndex] = 0.0f;
++promptIndex;
}
decoder.setInput(embeddings, "image_embeddings");
decoder.setInput(pointCoords, "point_coords");
decoder.setInput(pointLabels, "point_labels");
efficientSam.setInput(prep.blob, "batched_images");
efficientSam.setInput(pointCoords, "batched_point_coords");
efficientSam.setInput(pointLabels, "batched_point_labels");
std::vector<cv::Mat> outputs;
decoder.forward(outputs, std::vector<cv::String>{"scores", "masks"});
efficientSam.forward(outputs, std::vector<cv::String>{"output_masks", "iou_predictions"});
if (outputs.size() != 2)
return cv::Mat();
const float* scores = outputs[0].ptr<float>();
const int maskCount = static_cast<int>(outputs[0].total());
int bestScoreMask = 0;
for (int i = 1; i < maskCount; ++i) {
if (scores[i] > scores[bestScoreMask])
bestScoreMask = i;
}
cv::Mat lowMask(maskSize, maskSize, CV_32F, outputs[1].ptr<float>(0, bestScoreMask));
return LowMaskToFrameMask(lowMask, prep, frame.size(), modelSize, maskThreshold);
cv::Mat modelMask = SelectEfficientSamMask(outputs[0], outputs[1], backgroundPoints, maskThreshold);
if (modelMask.empty())
return cv::Mat();
return EfficientSamMaskToFrameMask(modelMask, frame.size(), maskThreshold);
}
bool CVObjectMask::SaveObjMaskData()
@@ -819,7 +885,6 @@ bool CVObjectMask::SaveObjMaskData()
return false;
}
google::protobuf::ShutdownProtobufLibrary();
return true;
}
@@ -858,14 +923,18 @@ void CVObjectMask::SetJsonValue(const Json::Value root)
{
if (!root["protobuf_data_path"].isNull())
protobufDataPath = root["protobuf_data_path"].asString();
if (!root["efficient_sam_model"].isNull())
efficientSamModelPath = root["efficient_sam_model"].asString();
if (!root["efficient_sam_model_path"].isNull())
efficientSamModelPath = root["efficient_sam_model_path"].asString();
if (!root["sam_model"].isNull())
efficientSamModelPath = root["sam_model"].asString();
if (!root["sam_model_path"].isNull())
efficientSamModelPath = root["sam_model_path"].asString();
if (!root["encoder_model"].isNull())
encoderModelPath = root["encoder_model"].asString();
efficientSamModelPath = root["encoder_model"].asString();
if (!root["encoder_model_path"].isNull())
encoderModelPath = root["encoder_model_path"].asString();
if (!root["decoder_model"].isNull())
decoderModelPath = root["decoder_model"].asString();
if (!root["decoder_model_path"].isNull())
decoderModelPath = root["decoder_model_path"].asString();
efficientSamModelPath = root["encoder_model_path"].asString();
if (!root["xmem_model_dir"].isNull())
xmemModelDir = root["xmem_model_dir"].asString();
if (!root["xmem_encode_key_model"].isNull())
@@ -885,14 +954,11 @@ void CVObjectMask::SetJsonValue(const Json::Value root)
if (!root["processing_device"].isNull())
processingDevice = root["processing_device"].asString();
if (!root["prompt_slots"].isNull())
promptSlots = std::max(1, root["prompt_slots"].asInt());
promptSlots = std::max(1, std::min(6, root["prompt_slots"].asInt()));
if (!root["mask_threshold"].isNull())
maskThreshold = root["mask_threshold"].asFloat();
if (!root["model_size"].isNull())
modelSize = root["model_size"].asInt();
if (!root["mask_size"].isNull())
maskSize = root["mask_size"].asInt();
promptKeyframes.clear();
if (!root["object_mask_selection"].isNull()) {
const Json::Value& selection = root["object_mask_selection"];
+7 -9
View File
@@ -53,16 +53,14 @@ namespace openshot
};
/**
* @brief Preprocess a clip into EdgeSAM object masks stored in the object-detection protobuf format.
* @brief Preprocess a clip into EfficientSAM/XMem object masks stored in the object-detection protobuf format.
*/
class CVObjectMask
{
private:
cv::dnn::Net encoder;
cv::dnn::Net decoder;
cv::dnn::Net efficientSam;
std::string encoderModelPath;
std::string decoderModelPath;
std::string efficientSamModelPath;
std::string xmemModelDir;
std::string xmemEncodeKeyModelPath;
std::string xmemEncodeValueModelPath;
@@ -71,10 +69,9 @@ namespace openshot
std::string processingDevice = "CPU";
std::map<size_t, CVObjectMaskPromptSet> promptKeyframes;
int promptSlots = 10;
int promptSlots = 6;
float maskThreshold = 0.0f;
int modelSize = 1024;
int maskSize = 256;
size_t start = 0;
size_t end = 0;
@@ -83,7 +80,7 @@ namespace openshot
ProcessingController* processingController;
void SetProcessingDevice();
cv::Mat CreateEdgeSAMSeedMask(const cv::Mat& frame, const CVObjectMaskPromptSet& prompts);
cv::Mat CreateEfficientSAMSeedMask(const cv::Mat& frame, const CVObjectMaskPromptSet& prompts);
void AddFrameDataToProto(pb_objdetect::Frame* pbFrameData, const CVObjectMaskFrameData& frameData);
public:
@@ -91,7 +88,8 @@ namespace openshot
CVObjectMask(std::string processInfoJson, ProcessingController& processingController);
static std::string ValidateONNXModels(std::string encoderPath, std::string decoderPath);
static std::string ValidateONNXModel(std::string modelPath);
std::shared_ptr<Frame> PreviewSeedMask(std::shared_ptr<Frame> frame);
void maskClip(openshot::Clip& video, size_t start = 0, size_t end = 0, bool process_interval = false);
bool SaveObjMaskData();
+12
View File
@@ -28,6 +28,18 @@ std::string ClipProcessingJobs::ValidateONNXModel(std::string modelPath){
#endif
}
std::shared_ptr<Frame> ClipProcessingJobs::PreviewObjectMask(std::string processInfoJson, std::shared_ptr<Frame> frame){
#ifdef USE_OPENCV
ProcessingController controller;
CVObjectMask objectMask(processInfoJson, controller);
return objectMask.PreviewSeedMask(frame);
#else
(void)processInfoJson;
(void)frame;
return std::shared_ptr<Frame>();
#endif
}
void ClipProcessingJobs::processClip(Clip& clip, std::string json){
processInfoJson = json;
+1
View File
@@ -60,6 +60,7 @@ class ClipProcessingJobs{
// Constructor
ClipProcessingJobs(std::string processingType, std::string processInfoJson);
static std::string ValidateONNXModel(std::string modelPath);
static std::shared_ptr<Frame> PreviewObjectMask(std::string processInfoJson, std::shared_ptr<Frame> frame);
// Process clip accordingly to processingType
void processClip(Clip& clip, std::string json);
+10
View File
@@ -16,6 +16,9 @@
#include "Frame.h"
#include "Json.h"
#include "effects/ObjectMask.h"
#ifdef USE_OPENCV
#include "CVObjectMask.h"
#endif
#include <QColor>
#include <QImage>
@@ -151,3 +154,10 @@ TEST_CASE("ObjectMask loads protobuf masks and exposes style controls", "[effect
std::remove(protobuf_path.c_str());
}
#ifdef USE_OPENCV
TEST_CASE("CVObjectMask validates a single EfficientSAM ONNX model path", "[effect][object_mask][opencv]") {
const std::string error = CVObjectMask::ValidateONNXModel("/tmp/libopenshot_missing_efficientsam.onnx");
CHECK(error.find("Failed to load ONNX model") != std::string::npos);
}
#endif