1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-31 00:03:03 +04:00

Merge pull request #28482 from satyam102006:core/yaml-1-2-support

core: add YAML 1.2 support for FileStorage #28482

Fixes: #26363
CI Update: https://github.com/opencv/ci-gha-workflow/pull/306

Summary: 
- Bool true/false literals support
- Header-less YAMLs support for Python YAML module compatibility. Header-less files are parsed as YAMLs by default
- Added FORMAT_YAML_1_0 flag to FileStorage for fallback.
- New YAML1.2 header

Testing: 
- Added Core_InputOutput.YAML_1_2_Compatibility test case in test_io.cpp.
- Added YAML interop test in Python
This commit is contained in:
satyam yadav
2026-05-19 16:14:50 +05:30
committed by GitHub
parent f85a662563
commit cc8f683b6a
20 changed files with 174 additions and 27 deletions
@@ -273,6 +273,7 @@ public:
FORMAT_XML = (1<<3), //!< flag, XML format
FORMAT_YAML = (2<<3), //!< flag, YAML format
FORMAT_JSON = (3<<3), //!< flag, JSON format
FORMAT_YAML_1_0 = (4<<3), //!< flag, Legacy YAML 1.0 format (strict headers, booleans as ints)
BASE64 = 64, //!< flag, write rawdata in Base64 by default. (consider using WRITE_BASE64)
WRITE_BASE64 = BASE64 | WRITE, //!< flag, enable both WRITE and BASE64
@@ -365,6 +366,8 @@ public:
*/
CV_WRAP void write(const String& name, int val);
/// @overload
CV_WRAP void write(const String& name, bool val);
CV_WRAP void write(const String& name, int64_t val);
/// @overload
CV_WRAP void write(const String& name, double val);
@@ -658,6 +661,7 @@ protected:
/////////////////// XML & YAML I/O implementation //////////////////
CV_EXPORTS void write( FileStorage& fs, const String& name, int value );
CV_EXPORTS void write( FileStorage& fs, const String& name, bool value );
CV_EXPORTS void write( FileStorage& fs, const String& name, int64_t value );
CV_EXPORTS void write( FileStorage& fs, const String& name, float value );
CV_EXPORTS void write( FileStorage& fs, const String& name, double value );
+31 -6
View File
@@ -695,9 +695,13 @@ bool FileStorage::Impl::open(const char *filename_or_buf, int _flags, const char
}
emitter_do_not_use_direct_dereference = createXMLEmitter(this);
} else if (fmt == FileStorage::FORMAT_YAML) {
if (!append)
puts("%YAML:1.0\n---\n");
} else if (fmt == FileStorage::FORMAT_YAML || fmt == FileStorage::FORMAT_YAML_1_0) {
if (!append) {
if (fmt == FileStorage::FORMAT_YAML_1_0)
puts("%YAML:1.0\n---\n"); // Legacy Flag -> Legacy Header
else
puts("%YAML 1.2\n---\n"); // Default -> Modern Header
}
else
puts("...\n---\n");
@@ -750,16 +754,15 @@ bool FileStorage::Impl::open(const char *filename_or_buf, int _flags, const char
char *bufPtr = cv_skip_BOM(buf);
size_t bufOffset = bufPtr - buf;
// NOTE: Yaml 1.2 allows to skip header. Data without type signature is read as YAML
if (strncmp(bufPtr, yaml_signature, strlen(yaml_signature)) == 0)
fmt = FileStorage::FORMAT_YAML;
else if (strncmp(bufPtr, json_signature, strlen(json_signature)) == 0)
fmt = FileStorage::FORMAT_JSON;
else if (strncmp(bufPtr, xml_signature, strlen(xml_signature)) == 0)
fmt = FileStorage::FORMAT_XML;
else if (strbufsize == bufOffset)
CV_Error(cv::Error::StsBadArg, "Input file is invalid");
else
CV_Error(cv::Error::StsBadArg, "Unsupported file storage format");
fmt = FileStorage::FORMAT_YAML;
rewind();
strbufpos = bufOffset;
@@ -782,6 +785,7 @@ bool FileStorage::Impl::open(const char *filename_or_buf, int _flags, const char
parser_do_not_use_direct_dereference = createXMLParser(this);
break;
case FileStorage::FORMAT_YAML:
case FileStorage::FORMAT_YAML_1_0:
parser_do_not_use_direct_dereference = createYAMLParser(this);
break;
case FileStorage::FORMAT_JSON:
@@ -2156,7 +2160,13 @@ void write( FileStorage& fs, const String& name, const String& value )
fs.p->write(name, value);
}
void write( FileStorage& fs, const String& name, bool value )
{
fs.p->write(name, value);
}
void FileStorage::write(const String& name, int val) { p->write(name, val); }
void FileStorage::write(const String& name, bool val) { p->write(name, val); }
void FileStorage::write(const String& name, int64_t val) { p->write(name, val); }
void FileStorage::write(const String& name, double val) { p->write(name, val); }
void FileStorage::write(const String& name, const String& val) { p->write(name, val); }
@@ -2894,6 +2904,21 @@ void read(const FileNode& node, std::string& val, const std::string& default_val
val = (std::string)node;
}
}
void FileStorage::Impl::write(const String &key, bool value)
{
CV_Assert(write_mode);
// [SWAP LOGIC]
if (fmt == FileStorage::FORMAT_YAML_1_0)
{
// Legacy behavior: Write as integer 1 or 0
getEmitter().write(key.c_str(), (int)value);
}
else
{
// Default/Modern behavior: Write as "true" or "false"
getEmitter().write(key.c_str(), value ? "true" : "false", false);
}
}
FileStorage_API::~FileStorage_API() {}
+2
View File
@@ -137,6 +137,8 @@ public:
virtual ~FileStorage_API();
virtual FileStorage* getFS() = 0;
virtual int getFormat() const = 0;
virtual void puts( const char* str ) = 0;
virtual char* gets() = 0;
virtual bool eof() = 0;
+3 -1
View File
@@ -69,6 +69,8 @@ public:
void write( const String& key, int value );
void write( const String& key, bool value );
void write( const String& key, int64_t value );
void write( const String& key, double value );
@@ -97,7 +99,7 @@ public:
FileNode operator[](const char* /*nodename*/) const;
int getFormat() const;
virtual int getFormat() const CV_OVERRIDE;
char* bufferPtr() const;
char* bufferStart() const;
+25 -2
View File
@@ -728,10 +728,30 @@ public:
if( is_parent_flow || c != ':' )
{
char* str_end = endptr;
// strip spaces in the end of string
do c = *--str_end;
while( str_end > ptr && c == ' ' );
str_end++;
if ((fs->getFormat() & FileStorage::FORMAT_MASK) != FileStorage::FORMAT_YAML_1_0)
{
size_t llen = str_end - ptr;
if (llen == 4 && memcmp(ptr, "true", 4) == 0) {
int64_t ival = 1;
node.setValue(FileNode::INT, &ival);
ptr = endptr;
return ptr;
}
else if (llen == 5 && memcmp(ptr, "false", 5) == 0) {
int64_t ival = 0;
node.setValue(FileNode::INT, &ival);
ptr = endptr;
return ptr;
}
else if (llen == 4 && memcmp(ptr, "null", 4) == 0) {
ptr = endptr;
return ptr;
}
}
node.setValue(FileNode::STRING, ptr, (int)(str_end - ptr));
ptr = endptr;
return ptr;
@@ -806,7 +826,10 @@ public:
if( memcmp( ptr, "%YAML", 5 ) == 0 &&
memcmp( ptr, "%YAML:1.", 8 ) != 0 &&
memcmp( ptr, "%YAML 1.", 8 ) != 0)
CV_PARSE_ERROR_CPP( "Unsupported YAML version (it must be 1.x)" );
{
if ((fs->getFormat() & FileStorage::FORMAT_MASK) == FileStorage::FORMAT_YAML_1_0)
CV_PARSE_ERROR_CPP( "Unsupported YAML version (it must be 1.x)" );
}
*ptr = '\0';
}
else if( *ptr == '-' )
+50 -4
View File
@@ -499,7 +499,7 @@ TEST(Core_InputOutput, FileStorageKey)
EXPECT_NO_THROW(f << "key1" << "value1");
EXPECT_NO_THROW(f << "_key2" << "value2");
EXPECT_NO_THROW(f << "key_3" << "value3");
const std::string expected = "%YAML:1.0\n---\nkey1: value1\n_key2: value2\nkey_3: value3\n";
const std::string expected = "%YAML 1.2\n---\nkey1: value1\n_key2: value2\nkey_3: value3\n";
ASSERT_STREQ(f.releaseAndGetString().c_str(), expected.c_str());
}
@@ -1199,7 +1199,7 @@ TEST(Core_InputOutput, FileStorage_DMatch)
EXPECT_NO_THROW(fs << "d" << d);
cv::String fs_result = fs.releaseAndGetString();
EXPECT_STREQ(fs_result.c_str(), "%YAML:1.0\n---\nd: [ 1, 2, 3, -1.5 ]\n");
EXPECT_STREQ(fs_result.c_str(), "%YAML 1.2\n---\nd: [ 1, 2, 3, -1.5 ]\n");
cv::FileStorage fs_read(fs_result, cv::FileStorage::READ | cv::FileStorage::MEMORY);
@@ -1227,7 +1227,7 @@ TEST(Core_InputOutput, FileStorage_DMatch_vector)
EXPECT_NO_THROW(fs << "dv" << dv);
cv::String fs_result = fs.releaseAndGetString();
EXPECT_STREQ(fs_result.c_str(),
"%YAML:1.0\n"
"%YAML 1.2\n"
"---\n"
"dv:\n"
" - [ 1, 2, 3, -1.5 ]\n"
@@ -1274,7 +1274,7 @@ TEST(Core_InputOutput, FileStorage_DMatch_vector_vector)
cv::String fs_result = fs.releaseAndGetString();
#ifndef OPENCV_TRAITS_ENABLE_DEPRECATED
EXPECT_STREQ(fs_result.c_str(),
"%YAML:1.0\n"
"%YAML 1.2\n"
"---\n"
"dvv:\n"
" -\n"
@@ -2278,4 +2278,50 @@ INSTANTIATE_TEST_CASE_P(Core_InputOutput,
FileStorage_exact_type, Values(".yml", ".xml", ".json")
);
TEST(Core_InputOutput, YAML_Compatibility)
{
string filename = cv::tempfile(".yaml");
// 1. Write using DEFAULT (should be 1.2 compatible now)
{
FileStorage fs(filename, FileStorage::WRITE | FileStorage::FORMAT_YAML);
ASSERT_TRUE(fs.isOpened());
fs << "bool_true" << true;
fs << "bool_false" << false;
fs.release();
}
// 2. Verify Default is Modern (Literals + No Legacy Header)
{
std::ifstream file(filename.c_str());
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
EXPECT_NE(content.find("bool_true: true"), std::string::npos); // Found 'true'
EXPECT_EQ(content.find("%YAML:1.0"), std::string::npos); // No 1.0 Header
}
// 3. Write using LEGACY flag
{
FileStorage fs(filename, FileStorage::WRITE | FileStorage::FORMAT_YAML_1_0);
ASSERT_TRUE(fs.isOpened());
fs << "bool_true" << true;
fs.release();
}
// 4. Verify Legacy (Integers + Strict Header)
{
std::ifstream file(filename.c_str());
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
EXPECT_NE(content.find("bool_true: 1"), std::string::npos); // Found '1'
EXPECT_NE(content.find("%YAML:1.0"), std::string::npos); // Found 1.0 Header
}
remove(filename.c_str());
}
}} // namespace
@@ -297,7 +297,7 @@ public class BruteForceDescriptorMatcherTest extends OpenCVTestCase {
matcher.write(filename);
String truth = "%YAML:1.0\n---\n";
String truth = "%YAML 1.2\n---\n";
assertEquals(truth, readFile(filename));
}
@@ -255,7 +255,7 @@ public class BruteForceHammingDescriptorMatcherTest extends OpenCVTestCase {
matcher.write(filename);
String truth = "%YAML:1.0\n---\n";
String truth = "%YAML 1.2\n---\n";
assertEquals(truth, readFile(filename));
}
@@ -250,7 +250,7 @@ public class BruteForceHammingLUTDescriptorMatcherTest extends OpenCVTestCase {
matcher.write(filename);
String truth = "%YAML:1.0\n---\n";
String truth = "%YAML 1.2\n---\n";
assertEquals(truth, readFile(filename));
}
@@ -261,7 +261,7 @@ public class BruteForceL1DescriptorMatcherTest extends OpenCVTestCase {
matcher.write(filename);
String truth = "%YAML:1.0\n---\n";
String truth = "%YAML 1.2\n---\n";
assertEquals(truth, readFile(filename));
}
@@ -273,7 +273,7 @@ public class BruteForceSL2DescriptorMatcherTest extends OpenCVTestCase {
matcher.write(filename);
String truth = "%YAML:1.0\n---\n";
String truth = "%YAML 1.2\n---\n";
assertEquals(truth, readFile(filename));
}
@@ -133,7 +133,7 @@ public class FASTFeatureDetectorTest extends OpenCVTestCase {
detector.write(filename);
String truth = "%YAML:1.0\n---\nname: \"Feature2D.FastFeatureDetector\"\nthreshold: 10\nnonmaxSuppression: 1\ntype: 2\n";
String truth = "%YAML 1.2\n---\nname: \"Feature2D.FastFeatureDetector\"\nthreshold: 10\nnonmaxSuppression: true\ntype: 2\n";
String data = readFile(filename);
assertEquals(truth, data);
@@ -51,7 +51,7 @@ public class FlannBasedDescriptorMatcherTest extends OpenCVTestCase {
+ " <type>8</type>\n" // FLANN_INDEX_TYPE_BOOL
+ " <value>1</value></_></searchParams>\n"
+ "</opencv_storage>\n";
static final String ymlParamsDefault = "%YAML:1.0\n---\n"
static final String ymlParamsDefault = "%YAML 1.2\n---\n"
+ "format: 3\n"
+ "indexParams:\n"
+ " -\n"
@@ -79,7 +79,7 @@ public class FlannBasedDescriptorMatcherTest extends OpenCVTestCase {
+ " name: sorted\n"
+ " type: 8\n" // FLANN_INDEX_TYPE_BOOL
+ " value: 1\n";
static final String ymlParamsModified = "%YAML:1.0\n---\n"
static final String ymlParamsModified = "%YAML 1.2\n---\n"
+ "format: 3\n"
+ "indexParams:\n"
+ " -\n"
@@ -58,7 +58,7 @@ public class GFTTFeatureDetectorTest extends OpenCVTestCase {
detector.write(filename);
String truth = "%YAML:1.0\n---\nname: \"Feature2D.GFTTDetector\"\nnfeatures: 1000\nqualityLevel: 0.01\nminDistance: 1.\nblockSize: 3\ngradSize: 3\nuseHarrisDetector: 0\nk: 0.040000000000000001\n";
String truth = "%YAML 1.2\n---\nname: \"Feature2D.GFTTDetector\"\nnfeatures: 1000\nqualityLevel: 0.01\nminDistance: 1.\nblockSize: 3\ngradSize: 3\nuseHarrisDetector: false\nk: 0.040000000000000001\n";
String actual = readFile(filename);
actual = actual.replaceAll("e([+-])0(\\d\\d)", "e$1$2"); // NOTE: workaround for different platforms double representation
assertEquals(truth, actual);
@@ -61,7 +61,7 @@ public class MSERFeatureDetectorTest extends OpenCVTestCase {
detector.write(filename);
String truth = "%YAML:1.0\n---\nname: \"Feature2D.MSER\"\ndelta: 5\nminArea: 60\nmaxArea: 14400\nmaxVariation: 0.25\nminDiversity: 0.20000000000000001\nmaxEvolution: 200\nareaThreshold: 1.01\nminMargin: 0.0030000000000000001\nedgeBlurSize: 5\npass2Only: 0\n";
String truth = "%YAML 1.2\n---\nname: \"Feature2D.MSER\"\ndelta: 5\nminArea: 60\nmaxArea: 14400\nmaxVariation: 0.25\nminDiversity: 0.20000000000000001\nmaxEvolution: 200\nareaThreshold: 1.01\nminMargin: 0.0030000000000000001\nedgeBlurSize: 5\npass2Only: false\n";
String actual = readFile(filename);
actual = actual.replaceAll("e([+-])0(\\d\\d)", "e$1$2"); // NOTE: workaround for different platforms double representation
assertEquals(truth, actual);
@@ -111,7 +111,7 @@ public class ORBDescriptorExtractorTest extends OpenCVTestCase {
extractor.write(filename);
String truth = "%YAML:1.0\n---\nname: \"Feature2D.ORB\"\nnfeatures: 500\nscaleFactor: 1.2000000476837158\nnlevels: 8\nedgeThreshold: 31\nfirstLevel: 0\nwta_k: 2\nscoreType: 0\npatchSize: 31\nfastThreshold: 20\n";
String truth = "%YAML 1.2\n---\nname: \"Feature2D.ORB\"\nnfeatures: 500\nscaleFactor: 1.2000000476837158\nnlevels: 8\nedgeThreshold: 31\nfirstLevel: 0\nwta_k: 2\nscoreType: 0\npatchSize: 31\nfastThreshold: 20\n";
// String truth = "%YAML:1.0\n---\n";
String actual = readFile(filename);
actual = actual.replaceAll("e\\+000", "e+00"); // NOTE: workaround for different platforms double representation
@@ -100,7 +100,7 @@ public class SIFTDescriptorExtractorTest extends OpenCVTestCase {
extractor.write(filename);
String truth = "%YAML:1.0\n---\nname: \"Feature2D.SIFT\"\nnfeatures: 0\nnOctaveLayers: 3\ncontrastThreshold: 0.040000000000000001\nedgeThreshold: 10.\nsigma: 1.6000000000000001\ndescriptorType: 5\n";
String truth = "%YAML 1.2\n---\nname: \"Feature2D.SIFT\"\nnfeatures: 0\nnOctaveLayers: 3\ncontrastThreshold: 0.040000000000000001\nedgeThreshold: 10.\nsigma: 1.6000000000000001\ndescriptorType: 5\n";
String actual = readFile(filename);
actual = actual.replaceAll("e([+-])0(\\d\\d)", "e$1$2"); // NOTE: workaround for different platforms double representation
assertEquals(truth, actual);
@@ -558,7 +558,7 @@ TEST( Features2d_DMatch, read_write )
#ifdef HAVE_OPENCV_FLANN
TEST( Features2d_FlannBasedMatcher, read_write )
{
static const char* ymlfile = "%YAML:1.0\n---\n"
static const char* ymlfile = "%YAML 1.2\n---\n"
"format: 3\n"
"indexParams:\n"
" -\n"
+2
View File
@@ -0,0 +1,2 @@
pyyml
numpy
+44 -1
View File
@@ -4,7 +4,7 @@ import tempfile
import os
import cv2 as cv
import numpy as np
from tests_common import NewOpenCVTests
from tests_common import NewOpenCVTests, unittest
class persistence_test(NewOpenCVTests):
@@ -42,3 +42,46 @@ class persistence_test(NewOpenCVTests):
fs.release()
os.remove(fname)
def test_yml_python_interop(self):
try:
import yaml
except:
raise unittest.SkipTest('Pyyml is not available for interop test')
ref_data = {
'int_value': 42,
"bool_value": True,
"float_value": 3.1415926,
"int64_value": 2147483647 + 1024, # C++ INT_MAX + 1024
"string_value": "opencv"
}
fd, test_file_name = tempfile.mkstemp(prefix="opencv_python_persistence_", suffix=".yml")
os.close(fd)
with open(test_file_name, 'w') as ff:
yaml.dump(ref_data, ff)
# Notice: no cv.FileStorage_FORMAT_YAML flag needed now thanks to the C++ fix!
fs = cv.FileStorage(test_file_name, cv.FILE_STORAGE_READ)
self.assertTrue(fs.isOpened())
node = fs.getNode('int_value')
self.assertTrue(node.isInt())
self.assertEqual(42, int(node.real()))
node = fs.getNode('int64_value')
self.assertTrue(node.isInt())
self.assertEqual(2147483647 + 1024, int(node.real()))
node = fs.getNode('float_value')
self.assertTrue(node.isReal())
self.assertEqual(3.1415926, node.real())
node = fs.getNode('string_value')
self.assertTrue(node.isString())
self.assertEqual("opencv", node.string())
fs.release()
os.remove(test_file_name)