Files
Jonathan Thomas 8bd98a592b 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
2026-05-18 19:18:56 -05:00

164 lines
4.9 KiB
C++

/**
* @file
* @brief Unit tests for Object Mask effect
* @author Jonathan Thomas <jonathan@openshot.org>
*
* @ref License
*/
// Copyright (c) 2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "openshot_catch.h"
#include "EffectInfo.h"
#include "Frame.h"
#include "Json.h"
#include "effects/ObjectMask.h"
#ifdef USE_OPENCV
#include "CVObjectMask.h"
#endif
#include <QColor>
#include <QImage>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <memory>
#include <unistd.h>
using namespace openshot;
static std::shared_ptr<Frame> make_object_mask_frame(int64_t number, int width, int height) {
auto frame = std::make_shared<Frame>(number, width, height, "#000000");
frame->GetImage()->fill(QColor(64, 64, 64, 255));
return frame;
}
static std::string temp_object_mask_path() {
char path[] = "/tmp/libopenshot_object_mask_XXXXXX";
int fd = mkstemp(path);
REQUIRE(fd != -1);
close(fd);
std::remove(path);
return std::string(path) + ".data";
}
static void append_varint(std::string& output, uint64_t value) {
while (value >= 0x80) {
output.push_back(static_cast<char>((value & 0x7f) | 0x80));
value >>= 7;
}
output.push_back(static_cast<char>(value));
}
static void append_fixed32_float(std::string& output, float value) {
uint32_t bits;
std::memcpy(&bits, &value, sizeof(float));
for (int i = 0; i < 4; ++i)
output.push_back(static_cast<char>((bits >> (8 * i)) & 0xff));
}
static void append_length_delimited(std::string& output, uint32_t field_number, const std::string& value) {
append_varint(output, (field_number << 3) | 2);
append_varint(output, static_cast<uint32_t>(value.size()));
output.append(value);
}
static std::string create_object_mask_data() {
const std::string path = temp_object_mask_path();
std::string mask;
append_varint(mask, 8);
append_varint(mask, 4);
append_varint(mask, 16);
append_varint(mask, 4);
for (uint32_t count : {0u, 6u, 10u}) {
append_varint(mask, 24);
append_varint(mask, count);
}
std::string box;
append_varint(box, 13);
append_fixed32_float(box, 0.0f);
append_varint(box, 21);
append_fixed32_float(box, 0.0f);
append_varint(box, 29);
append_fixed32_float(box, 1.0f);
append_varint(box, 37);
append_fixed32_float(box, 1.0f);
append_varint(box, 40);
append_varint(box, 0);
append_varint(box, 53);
append_fixed32_float(box, 0.95f);
append_varint(box, 56);
append_varint(box, 1);
append_length_delimited(box, 8, mask);
std::string frame;
append_varint(frame, 8);
append_varint(frame, 1);
append_length_delimited(frame, 2, box);
std::string data;
append_length_delimited(data, 1, frame);
append_length_delimited(data, 3, "object mask");
std::ofstream output(path, std::ios::out | std::ios::binary);
output.write(data.data(), static_cast<std::streamsize>(data.size()));
REQUIRE(output.good());
return path;
}
TEST_CASE("ObjectMask effect is registered", "[effect][object_mask]") {
std::unique_ptr<EffectBase> effect(EffectInfo().CreateEffect("ObjectMask"));
REQUIRE(effect != nullptr);
CHECK(effect->info.name == "Object Mask");
CHECK(effect->info.has_video);
CHECK(effect->info.has_tracked_object);
}
TEST_CASE("ObjectMask loads protobuf masks and exposes style controls", "[effect][object_mask]") {
const std::string protobuf_path = create_object_mask_data();
ObjectMask effect;
Json::Value config;
config["protobuf_data_path"] = protobuf_path;
config["mask_alpha"] = Keyframe(0.5).JsonValue();
config["stroke_width"] = Keyframe(2.0).JsonValue();
effect.SetJsonValue(config);
Json::Value properties = stringToJson(effect.PropertiesJSON(1));
CHECK(properties["draw_mask"]["name"].asString() == "Draw Mask");
CHECK(properties["mask_color"]["name"].asString() == "Mask Color");
CHECK(properties["mask_alpha"]["value"].asDouble() == Approx(0.5));
CHECK(properties["stroke_color"]["name"].asString() == "Stroke Color");
CHECK(properties["stroke_alpha"]["name"].asString() == "Stroke Alpha");
CHECK(properties["stroke_width"]["value"].asDouble() == Approx(2.0));
auto generated_mask = effect.TrackedObjectMask(std::make_shared<QImage>(4, 4, QImage::Format_RGBA8888_Premultiplied), 1);
REQUIRE(generated_mask != nullptr);
CHECK(generated_mask->pixelColor(0, 0).red() == 255);
CHECK(generated_mask->pixelColor(3, 3).red() == 0);
Json::Value no_stroke;
no_stroke["stroke_width"] = Keyframe(0.0).JsonValue();
effect.SetJsonValue(no_stroke);
auto frame = make_object_mask_frame(1, 4, 4);
auto output = effect.GetFrame(frame, 1)->GetImage();
CHECK(output->pixelColor(0, 0) != QColor(64, 64, 64, 255));
CHECK(output->pixelColor(3, 3) == QColor(64, 64, 64, 255));
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