1

I assume my camera calibration, performed with EmguCV library, is successful because I can apply the calibration to images and they appear to be at least somewhat corrected. Now I want to store the calibration to disk so I can simply load it when my program runs. IntrinsicCameraParameters has methods called writeXML() and readXML() that take XmlWriter and XmlReader as arguments, respectively. Seems like the way to go. I found an example where someone just instantiated a default XmlWriter and immediately used it to call WriteXml(). But when I try this I get runtime exceptions that seem to be related to the structure of the XML (i.e. only able to have one root node in the XML). So I have adjusted the code and shared it below. If I don't include the stupid root element then the call to "WriteXml" throws the exception about improperly formed XML. So I seem to be able to write it, but I don't know how to read it back. Perhaps the stupid root element prevents the read from succeeding. Can't find any examples of anyone reading it back. Does any one have an example to share?

public void SaveCalibrationToFile(IntrinsicCameraParameters ICP)
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.ConformanceLevel = ConformanceLevel.Auto;

            // DISTORTION COEFFICIENTS
            XmlWriter writer = XmlWriter.Create("C:\\Video\\Cal\\DistortionCoeff.xml", settings);               
            writer.WriteStartDocument();
            writer.WriteStartElement("TheStupidRootElement");

                writer.WriteStartElement("TheDistortionCoefficients");
                ICP.DistortionCoeffs.WriteXml(writer);
                writer.WriteEndElement(); // end TheDistortionCoefficients

            writer.WriteEndElement(); // end TheStupidRootElement
            writer.WriteEndDocument();
            writer.Close();

            // CAMERA MATRIX
            writer = XmlWriter.Create("C:\\Video\\Cal\\CameraMatrix.xml", settings);                
            writer.WriteStartDocument();
            writer.WriteStartElement("TheStupidRootElement");

                writer.WriteStartElement("TheCameraMatrix");
                ICP.IntrinsicMatrix.WriteXml(writer);
                writer.WriteEndElement(); // end TheCameraMatrix

            writer.WriteEndElement(); // end TheStupidRootElement
            writer.WriteEndDocument();
            writer.Close();

            // now [just to see if it worked] try to load from the XML
            XmlReaderSettings readerSettings = new XmlReaderSettings();
            readerSettings.ConformanceLevel = ConformanceLevel.Auto;
            XmlReader reader = XmlReader.Create("C:\\Video\\Cal\\DistortionCoeff.xml", readerSettings);

            IntrinsicCameraParameters ICP_read = new IntrinsicCameraParameters();
            IC.DistortionCoeffs.ReadXml(reader);                
        }
Kyle Sweet
  • 306
  • 1
  • 3
  • 15
  • [`IntrinsicCameraParameters.DistortionCoeffs`](http://www.emgu.com/wiki/files/2.0.0.0/html/ecc3c8f8-9f92-10b1-96a2-b0b1ca6501e9.htm) actually implements [`IXmlSerializable`](https://msdn2.microsoft.com/en-us/library/fhd7bk0a), so have you tried simply using [`XmlSerializer`](https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer(v=vs.110).aspx) on your [`IntrinsicCameraParameters`](http://www.emgu.com/wiki/files/2.0.0.0/html/1ad4e83e-3c4b-d128-cd44-c9e8a9da007f.htm) ? – dbc Aug 31 '16 at 21:51
  • I have not tried that. I'm not very good with XML. Can you elaborate on how XmlSerializer would be used? – Kyle Sweet Aug 31 '16 at 22:07

1 Answers1

1

.Net contains a class XmlSerializer that can automatically serialize instances of types from and to XML via reflection of public properties. It also supports the IXmlSerializable interface to allow types to override its default behavior and take complete control of how they are serialized to XML.

As it turns out, IntrinsicCameraParameters.IntrinsicMatrix and IntrinsicCameraParameters.DistortionCoeffs are both of type Matrix<double> whose base class CvArray<TDepth> implements this interface. Thus objects of this type should be serializeable to XML using XmlSerializer using the following extension methods:

public static partial class XmlSerializationExtensions
{
    public static void SerializeToXmlFile<T>(this T obj, string fileName)
    {
        var settings = new XmlWriterSettings { Indent = true };
        using (var writer = XmlWriter.Create(fileName, settings))
        {
            new XmlSerializer(typeof(T)).Serialize(writer, obj);
        }
    }

    public static T DeserializeFromXmlFile<T>(string fileName)
    {
        using (var reader = XmlReader.Create(fileName))
        {
            return (T)new XmlSerializer(typeof(T)).Deserialize(reader);
        }
    }
}

Then you can just do:

var fileName = "C:\\Video\\Cal\\CameraMatrix.xml";
ICP.IntrinsicMatrix.SerializeToXmlFile(fileName);

And later, to read it back, do:

var newIntrinsicMatrix = XmlSerializationExtensions.DeserializeFromXmlFile<Matrix<double>>(fileName);

For confirmation, see the documentation for another example of serializing a matrix from and to XML.

Logically, it should also be possible to save and restore the entire IntrinsicCameraParameters in a single XML file using the same generic extension method:

ICP.SerializeToXmlFile(fileName);

Unfortunately, there's a problem. The implementation of ReadXml() for CvArray<TDepth> is broken. From Proper way to implement IXmlSerializable?

The ReadXml method must reconstitute your object using the information that was written by the WriteXml method.

When this method is called, the reader is positioned at the start of the element that wraps the information for your type. That is, just before the start tag that indicates the beginning of a serialized object. When this method returns, it must have read the entire element from beginning to end, including all of its contents. Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.

And a quick check of the source code for CvArray<TDepth> shows that the end of the wrapper element is not read. This will cause any data subsequent to the first CvArray<> in the XML to fail to be deserialized.

Thus, if you want to embed a Matrix<T> in a larger XML file, you're going to need to introduce serialization surrogate types (or data transfer object types if you prefer), like the following. (Note the introduction of the Emgu.CV.SerializationSurrogates namespace):

namespace Emgu.CV.SerializationSurrogates
{
    using Emgu.CV;

    public class Matix<TDepth> where TDepth : new()
    {
        [XmlAttribute]
        public int Rows { get; set; }

        [XmlAttribute]
        public int Cols { get; set; }

        [XmlAttribute]
        public int NumberOfChannels { get; set; }

        [XmlAttribute]
        public int CompressionRatio { get; set; }

        public byte[] Bytes { get; set; }

        public static implicit operator Emgu.CV.SerializationSurrogates.Matix<TDepth>(Emgu.CV.Matrix<TDepth> matrix)
        {
            if (matrix == null)
                return null;
            return new Matix<TDepth>
            {
                Rows = matrix.Rows,
                Cols = matrix.Cols,
                NumberOfChannels = matrix.NumberOfChannels,
                CompressionRatio = matrix.SerializationCompressionRatio,
                Bytes = matrix.Bytes,
            };
        }

        public static implicit operator Emgu.CV.Matrix<TDepth>(Matix<TDepth> surrogate)
        {
            if (surrogate == null)
                return null;
            var matrix = new Emgu.CV.Matrix<TDepth>(surrogate.Rows, surrogate.Cols, surrogate.NumberOfChannels);
            matrix.SerializationCompressionRatio = surrogate.CompressionRatio;
            matrix.Bytes = surrogate.Bytes;
            return matrix;
        }
    }

    public class IntrinsicCameraParameters
    {
        [XmlElement("IntrinsicMatrix", Type = typeof(Emgu.CV.SerializationSurrogates.Matix<double>))]
        public Emgu.CV.Matrix<double> IntrinsicMatrix { get; set; }

        [XmlElement("DistortionCoeffs", Type = typeof(Emgu.CV.SerializationSurrogates.Matix<double>))]
        public Emgu.CV.Matrix<double> DistortionCoeffs { get; set; }

        public static implicit operator Emgu.CV.SerializationSurrogates.IntrinsicCameraParameters(Emgu.CV.IntrinsicCameraParameters icp)
        {
            if (icp == null)
                return null;
            return new IntrinsicCameraParameters
            {
                DistortionCoeffs = icp.DistortionCoeffs,
                IntrinsicMatrix = icp.IntrinsicMatrix,
            };
        }

        public static implicit operator Emgu.CV.IntrinsicCameraParameters(Emgu.CV.SerializationSurrogates.IntrinsicCameraParameters surrogate)
        {
            if (surrogate == null)
                return null;
            return new Emgu.CV.IntrinsicCameraParameters
            {
                DistortionCoeffs = surrogate.DistortionCoeffs,
                IntrinsicMatrix = surrogate.IntrinsicMatrix,
            };
        }
    }
}

Now you can save and retrieve your IntrinsicCameraParameters with the following extension methods:

public static class IntrinsicCameraParametersExtensions
{
    public static void SerializeIntrinsicCameraParametersExtensionsToXmlFile(this IntrinsicCameraParameters icp, string fileName)
    {
        var surrogate = (Emgu.CV.SerializationSurrogates.IntrinsicCameraParameters)icp;
        surrogate.SerializeToXmlFile(fileName);
    }

    public static IntrinsicCameraParameters DeserializeIntrinsicCameraParametersFromXmlFile(string fileName)
    {
        var surrogate = XmlSerializationExtensions.DeserializeFromXmlFile<Emgu.CV.SerializationSurrogates.IntrinsicCameraParameters>(fileName);
        return surrogate;
    }
}

All that being said, IntrinsicCameraParameters is marked as obsolete in the current release:

[SerializableAttribute]
[ObsoleteAttribute("This class will be removed in the next release, please use separate camera matrix and distortion coefficient with the CvInvoke function instead.")]
public class IntrinsicCameraParameters : IEquatable<IntrinsicCameraParameters>

So you might want to re-think this design.

Incidentally, a non-broken version of CvArray<TDepth>.ReadXml() would look like:

    public virtual void ReadXml(System.Xml.XmlReader reader)
    {
        #region read properties of the matrix and assign storage

        int rows = Int32.Parse(reader.GetAttribute("Rows")); // Should really be using XmlConvert for this
        int cols = Int32.Parse(reader.GetAttribute("Cols"));
        int numberOfChannels = Int32.Parse(reader.GetAttribute("NumberOfChannels"));
        SerializationCompressionRatio = Int32.Parse(reader.GetAttribute("CompressionRatio"));

        AllocateData(rows, cols, numberOfChannels);

        #endregion

        #region decode the data from Xml and assign the value to the matrix

        if (!reader.IsEmptyElement)
        {
            using (var subReader = reader.ReadSubtree())
            {
                // Using ReadSubtree guarantees we don't read past the end of the element in case the <Bytes> element
                // is missing or extra unexpected elements are included.
                if (subReader.ReadToFollowing("Bytes"))
                {
                    int size = _sizeOfElement * ManagedArray.Length;
                    if (SerializationCompressionRatio == 0)
                    {
                        Byte[] bytes = new Byte[size];
                        subReader.ReadElementContentAsBase64(bytes, 0, bytes.Length);
                        Bytes = bytes;
                    }
                    else
                    {
                        int extraHeaderBytes = 20000;
                        Byte[] bytes = new Byte[size + extraHeaderBytes];
                        int countOfBytesRead = subReader.ReadElementContentAsBase64(bytes, 0, bytes.Length);
                        Array.Resize<Byte>(ref bytes, countOfBytesRead);
                        Bytes = bytes;
                    }
                }
            }
        }
        // Consume the end of the wrapper element also.
        reader.Read();

        #endregion
    }
dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks for all the work put in this answer. I got it working with the 2-file solution from your previous [now deleted] answer. I don't want to spend more hours to get to a 1-file future-proof solution. The C# tool is just a test bed and ultimately the thing I'm creating will be implemented in regular OpenCV with C++. I feel like Emgu is a mess and the fact that I'm even trying to work with OpenCV in C# may have been a bad idea to begin with. I thought it'd be easy because it's C# but I'm a whole week into this already. My coworker wrote the same thing in python in like two days. – Kyle Sweet Sep 02 '16 at 15:57