Как элегантно сериализовать и десериализовать данные калибровки OpenCV YAML в Java? - PullRequest
3 голосов
/ 20 апреля 2020

Я пытаюсь загрузить / сохранить данные калибровки OpenCV в формате YAML, используя официальные привязки OpenCV Java. Я знаю, что OpenCV (по крайней мере, версия c ++) может сериализоваться до XML и JSON, но я хотел бы поддерживать более старые файлы калибровки YAML.

Файл калибровки выглядит следующим образом:

%YAML:1.0
cameraMatrix: !!opencv-matrix
   rows: 3
   cols: 3
   dt: d
   data: [ 6.6278599887122368e+02, 0., 3.1244256016006659e+02, 0.,
       6.6129276875199082e+02, 2.2747179767124251e+02, 0., 0., 1. ]
imageSize_width: 640
imageSize_height: 480
sensorSize_width: 0
sensorSize_height: 0
distCoeffs: !!opencv-matrix
   rows: 5
   cols: 1
   dt: d
   data: [ -1.8848338341464690e-01, 1.0721890419183855e+00,
       -3.5244467228016116e-03, -7.0195032848241403e-04,
       -2.0412827999027101e+00 ]
reprojectionError: 2.1723265945911407e-01

Я уже посмотрел несколько ответов здесь и здесь , однако я ищу элегантное решение, так как я не совсем понял лучшую карту java занятия в YAML и обратно. Я пробовал несколько библиотек, таких как jyaml, yamlbeans (обе версии 1.0 от SourceForge и 1.13 через Maven Central) и SnakeYAML.

Моя текущая попытка десериализации такого рода работ, но мне кажется, что она довольно хакерская:

CalibrationParseTest. java

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

import org.opencv.core.Core;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

public class CalibrationParseTest {

    public static void main(String[] args) {
        // load OpenCV native
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

        String yamlPath = "./data/calibration.yml";

        try{

          String yamlString = new String(Files.readAllBytes(Paths.get(yamlPath)), StandardCharsets.UTF_8);
          // remove %YAML:1.0 to avoid scan directive error
          yamlString = yamlString.replaceAll("%YAML:1.0", "");
          // map custom class
          yamlString = yamlString.replaceAll("opencv-matrix", "MatYAML");

          System.out.println("<loaded>");
          System.out.println(yamlString);
          System.out.println("</loaded>");

          Yaml yaml = new Yaml(new Constructor(CalibrationData.class));
          CalibrationData data = yaml.load(yamlString);
          // currently manually parsing data from the HashMap: can this be better ?
          data.populateCV();
          // double check data
          System.out.println("<deserialized>");
          System.out.println(data);
          System.out.println("</deserialized>");

        }catch (IOException e) { 
          e.printStackTrace(); 
        } 
    }

}

CalibrationData. java

import java.util.HashMap;

import org.opencv.core.Mat;
import org.opencv.core.Size;

public class CalibrationData extends HashMap{

    public Mat cameraMatrix;
    public Size imageSize;
    public Size sensorSize;
    public Mat distCoeffs;
    public float reprojectionError;

    public CalibrationData(){}

    public void populateCV(){
        cameraMatrix      = ((MatYAML)get("cameraMatrix")).toMat();
        imageSize         = new Size((int)get("imageSize_width"),(int)get("imageSize_height"));
        sensorSize        = new Size((int)get("sensorSize_width"),(int)get("sensorSize_height"));
        distCoeffs        = ((MatYAML)get("distCoeffs")).toMat();
        reprojectionError = (float)((double)get("reprojectionError"));
    }

    public String toString(){
        if(cameraMatrix == null){
            return String.format("[CalibrationData (not parsed to CV-> call populateCV()\n\tdata: %s\n]",super.toString());
        }
        return String.format("[CalibrationData\n" + 
                             "\tcalibrationMatrix: %s\n" + 
                             "\timageSize: %s\n" + 
                             "\tsensorSize: %s\n" + 
                             "\tdistCoeffs: %s\n" + 
                             "\treprojectionError: %f\n]", cameraMatrix.dump(), imageSize.toString(), sensorSize.toString(), distCoeffs.dump(), reprojectionError);
    }

}

MatYAML. java

import java.util.List;

import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class MatYAML{

    public int rows;
    public int cols;
    public String dt;
    public List<Double> data;

    Mat toMat(){
        Mat out = new Mat(rows, cols, dt.equals("d") ? CvType.CV_64F : CvType.CV_32F);

        int index = 0;

        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                out.put(row, col, data.get(index++));
            }
        }

        return out;
    }

}

Выводит ожидаемый результат:

<loaded>

cameraMatrix: !!MatYAML
   rows: 3
   cols: 3
   dt: d
   data: [ 6.6278599887122368e+02, 0., 3.1244256016006659e+02, 0.,
       6.6129276875199082e+02, 2.2747179767124251e+02, 0., 0., 1. ]
imageSize_width: 640
imageSize_height: 480
sensorSize_width: 0
sensorSize_height: 0
distCoeffs: !!MatYAML
   rows: 5
   cols: 1
   dt: d
   data: [ -1.8848338341464690e-01, 1.0721890419183855e+00,
       -3.5244467228016116e-03, -7.0195032848241403e-04,
       -2.0412827999027101e+00 ]
reprojectionError: 2.1723265945911407e-01

</loaded>
<deserialized>
[CalibrationData
    calibrationMatrix: [662.7859988712237, 0, 312.4425601600666;
  0, 661.2927687519908, 227.4717976712425;
  0, 0, 1]
    imageSize: 640x480
    sensorSize: 0x0
    distCoeffs: [-0.1884833834146469; 1.072189041918385; -0.003524446722801612; -0.000701950328482414; -2.04128279990271]
    reprojectionError: 0.217233
]
</deserialized>

Есть ли более элегантный способ сериализации / десериализации между Java классами OpenCV и YAML без этих хаков?

Под хаки я имею в виду:

  • вручную удаление директивы yaml версии
  • замена opencv-матрицы строкой MatYAML
  • приведение значений HashMap вручную
  • , потенциально не позволяющих вручную заполнять данные OpenCV Mat? (если возможно?)

Обновление 2 Ответ Аманина чище и позволяет избежать случайного замещения "!! opencv-matrix", однако он не сериализует / десериализует Mat:

OpenCVConfig{imageSize_width=640, imageSize_height=480, sensorSize_width=0, sensorSize_height=0, camerMatrix=Matrix{rows=3, cols=3, dt=d, data=[662.7859988712237, 0.0, 312.4425601600666, 0.0, 661.2927687519908, 227.4717976712425, 0.0, 0.0, 1.0]}, distCoeffs=Matrix{rows=5, cols=1, dt=d, data=[-0.1884833834146469, 1.0721890419183855, -0.0035244467228016116, -7.01950328482414E-4, -2.04128279990271]}}
---
imageSize_width: 640
imageSize_height: 480
sensorSize_width: 0
sensorSize_height: 0
reprojectionError: 0.21723265945911407
cameraMatrix:
  rows: 3
  cols: 3
  dt: "d"
  data:
 - 662.7859988712237
 - 0.0
 - 312.4425601600666
 - 0.0
 - 661.2927687519908
 - 227.4717976712425
 - 0.0
 - 0.0
 - 1.0
distCoeffs:
  rows: 5
  cols: 1
  dt: "d"
  data:
 - -0.1884833834146469
 - 1.0721890419183855
 - -0.0035244467228016116
 - -7.01950328482414E-4
 - -2.04128279990271

Пожалуйста, сообщите об интеграции решения с org.opencv.core.Mat

1 Ответ

5 голосов
/ 22 апреля 2020

Вы смотрели на библиотеку Джексона? Он позволяет отображать содержимое JSON / Yaml в Java POJO.

Я сделал небольшой пример, который решает две ваши проблемы:

  • замена opencv-matrix строкой MatYAML
  • приведение значений HashMap вручную

Однако для директивы версии yaml, поскольку она выглядит недопустимой, я не уверен, как с ней обращаться. В моем примере, я удалил его вручную до этого. Конечно, может быть найдено лучшее решение, но я не знаю его.

EDIT2 : Для матричного объекта я сделал тупой POJO, используемый Джексоном для чтения изнутри брют ямл. Затем я добавил слой преобразования (см. @ JsonSerialize и @ JsonDeserialize аннотации в OpenCVConfig класс) для преобразования этого простого POJO в специализированную матрицу OpenCV. Компания Jackson предлагает различную технику (потоковую передачу, настраиваемые конвертеры / десериализаторы, направляющие аннотации и т. Д. c.) Картографирования, поэтому вы можете изучить ее возможности и найти решение, наиболее подходящее для вас.

Итак, чтобы чтобы пример работал, вам понадобятся две зависимости (приведенные здесь в формате maven):

        <!-- (De)serialization engine -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.0</version>
        </dependency>

        <!-- Yaml support -->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.10.0</version>
        </dependency>

Вот страницы описания репозитория maven:

Jackson-databind : Через поиск maven или Через mvnrepository

Jackson-dataformat-yaml : Через поиск maven или Через mvnrepository

Примечание : Мой пример содержит множество шаблонов, как я вручную (ну, IDE) генерировал геттеры / сеттеры. Вы должны значительно уменьшить количество кода:

  • Использование Lombok lib
  • Кодирование с Kotlin (классы данных)
  • Использование атрибутов publi c без getter / setter
  • Использование java 14 записи (не уверен, работает ли она в это время)
package fr.amanin.stackoverflow;

import java.util.Arrays;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class YAMLOpenCV {
    /**
     * Engine in charge of YAML decoding.
     */
    public static final ObjectMapper OPENCV_YAML_MAPPER = new YAMLMapper();

    public static void main(String[] args) throws Exception {
        nu.pattern.OpenCV.loadShared();
        System.loadLibrary(org.opencv.core.Core.NATIVE_LIBRARY_NAME);
        final String confStr =
                "cameraMatrix: !!opencv-matrix\n" +
                        "   rows: 3\n" +
                        "   cols: 3\n" +
                        "   dt: d\n" +
                        "   data: [ 6.6278599887122368e+02, 0., 3.1244256016006659e+02, 0.,\n" +
                        "       6.6129276875199082e+02, 2.2747179767124251e+02, 0., 0., 1. ]\n" +
                        "imageSize_width: 640\n" +
                        "imageSize_height: 480\n" +
                        "sensorSize_width: 0\n" +
                        "sensorSize_height: 0\n" +
                        "distCoeffs: !!opencv-matrix\n" +
                        "   rows: 5\n" +
                        "   cols: 1\n" +
                        "   dt: d\n" +
                        "   data: [ -1.8848338341464690e-01, 1.0721890419183855e+00,\n" +
                        "       -3.5244467228016116e-03, -7.0195032848241403e-04,\n" +
                        "       -2.0412827999027101e+00 ]\n" +
                        "reprojectionError: 2.1723265945911407e-01";

        OpenCVConfig conf = OPENCV_YAML_MAPPER.readValue(confStr, OpenCVConfig.class);
        System.out.println(conf);

        String serialized = OPENCV_YAML_MAPPER.writeValueAsString(conf);
        System.out.println(serialized);
    }

    /**
     * Java model mirroring YAML configuration. Jackson will fill it 
     * with values read from YAML configuration file, matching YAML 
     * fields with this class property names.
     */
    public static class OpenCVConfig {
        int imageSize_width;
        int imageSize_height;
        int sensorSize_width;
        int sensorSize_height;

        double reprojectionError;

        /* Special case: Matrix objects are decoded in two passes:
         * 1. Jackson will check below converters, and use their input 
         * type to decode YAML to `Matrix` object (intermediate step). 
         * 2. Jackson uses converter to delegate to user the mapping 
         * from this intermediate POJO to specialized target (`Mat` here) 
         */
        @JsonDeserialize(converter = ToMatConverter.class)
        @JsonSerialize(converter = FromMatConverter.class)
        Mat cameraMatrix;
        @JsonDeserialize(converter = ToMatConverter.class)
        @JsonSerialize(converter = FromMatConverter.class)
        Mat distCoeffs;

        public int getImageSize_width() {
            return imageSize_width;
        }

        public OpenCVConfig setImageSize_width(int imageSize_width) {
            this.imageSize_width = imageSize_width;
            return this;
        }

        public int getImageSize_height() {
            return imageSize_height;
        }

        public OpenCVConfig setImageSize_height(int imageSize_height) {
            this.imageSize_height = imageSize_height;
            return this;
        }

        public int getSensorSize_width() {
            return sensorSize_width;
        }

        public OpenCVConfig setSensorSize_width(int sensorSize_width) {
            this.sensorSize_width = sensorSize_width;
            return this;
        }

        public int getSensorSize_height() {
            return sensorSize_height;
        }

        public OpenCVConfig setSensorSize_height(int sensorSize_height) {
            this.sensorSize_height = sensorSize_height;
            return this;
        }

        public double getReprojectionError() {
            return reprojectionError;
        }

        public OpenCVConfig setReprojectionError(double reprojectionError) {
            this.reprojectionError = reprojectionError;
            return this;
        }

        public Mat getCameraMatrix() {
            return cameraMatrix;
        }

        public OpenCVConfig setCameraMatrix(Mat cameraMatrix) {
            this.cameraMatrix = cameraMatrix;
            return this;
        }

        public Mat getDistCoeffs() {
            return distCoeffs;
        }

        public OpenCVConfig setDistCoeffs(Mat distCoeffs) {
            this.distCoeffs = distCoeffs;
            return this;
        }

        @Override
        public String toString() {
            return "OpenCVConfig{" +
                    "imageSize_width=" + imageSize_width +
                    ", imageSize_height=" + imageSize_height +
                    ", sensorSize_width=" + sensorSize_width +
                    ", sensorSize_height=" + sensorSize_height +
                    ", camerMatrix=" + cameraMatrix +
                    ", distCoeffs=" + distCoeffs +
                    '}';
        }
    }

    /**
     * Converter used for serialization of Mat objects into YAML.
     */
    private static class FromMatConverter implements Converter<Mat, Matrix> {

        @Override
        public Matrix convert(Mat value) {
            final Matrix result = new Matrix();
            result.cols = value.cols();
            result.rows = value.rows();
            final int type = value.type();
            result.dt = Stream.of(MatrixDataType.values())
                    .filter(dt -> dt.mapping == type)
                    .findAny()
                    .orElseThrow(() -> new UnsupportedOperationException("No matching datatype found for "+type));
            int idx = 0;
            result.data = new double[result.rows * result.cols];
            for (int r = 0 ; r < result.rows ; r++) {
                for (int c = 0; c < result.cols; c++) {
                    final double[] v = value.get(r, c);
                    result.data[idx++] = v[0];
                }
            }
            return result;
        }

        @Override
        public JavaType getInputType(TypeFactory typeFactory) {
            return typeFactory.constructType(new TypeReference<Mat>() {});
        }

        @Override
        public JavaType getOutputType(TypeFactory typeFactory) {
            return typeFactory.constructType(new TypeReference<Matrix>() {});
        }
    }

    /**
     * Converter used at read time, to map YAML object to OpenCV Mat.
     */
    private static class ToMatConverter implements Converter<Matrix, Mat> {

        @Override
        public Mat convert(Matrix in) {
            final Mat result = new Mat(in.rows, in.cols, in.dt.mapping);

            int idx = 0;
            for (int r = 0 ; r < in.rows ; r++) {
                for (int c = 0; c < in.cols; c++) {
                    result.put(r, c, in.data[idx++]);
                }
            }

            return result;
        }

        @Override
        public JavaType getInputType(TypeFactory typeFactory) {
            return typeFactory.constructType(new TypeReference<Matrix>() {});
        }

        @Override
        public JavaType getOutputType(TypeFactory typeFactory) {
            return typeFactory.constructType(new TypeReference<Mat>() {});
        }
    }

    public static class Matrix {
        int rows;
        int cols;
        MatrixDataType dt;
        double[] data;

        public int getRows() {
            return rows;
        }

        public Matrix setRows(int rows) {
            this.rows = rows;
            return this;
        }

        public int getCols() {
            return cols;
        }

        public Matrix setCols(int cols) {
            this.cols = cols;
            return this;
        }

        public MatrixDataType getDt() {
            return dt;
        }

        public Matrix setDt(MatrixDataType dt) {
            this.dt = dt;
            return this;
        }

        public double[] getData() {
            return data;
        }

        public Matrix setData(double[] data) {
            this.data = data;
            return this;
        }

        double at(int x, int y) {
            if (x >= cols || y >= rows) throw new IllegalArgumentException("Bad coordinate");
            return data[y*rows + x];
        }

        @Override
        public String toString() {
            return "Matrix{" +
                    "rows=" + rows +
                    ", cols=" + cols +
                    ", dt=" + dt +
                    ", data=" + Arrays.toString(data) +
                    '}';
        }
    }
/*
    public static class MatDeserializer extends StdDeserializer<Mat> {

        protected MatDeserializer() {
            super(Mat.class);
        }

        @Override
        public Mat deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            final int rows, cols;
            final MatrixDataType dtype;
            final double[] data;
        }
    }

    public static class MatSerializer extends StdSerializer<Mat> {

        protected MatSerializer() {
            super(Mat.class);
        }

        @Override
        public void serialize(Mat value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            gen.writeNumberField("rows", value.rows());
            gen.writeNumberField("cols", value.cols());
            gen.writeFieldName("data");
            gen.writeStartArray();
            gen.writeEndArray();
        }
    }
*/
    public enum MatrixDataType {
        d(CvType.CV_64F),
        f(CvType.CV_32F);

        public final int mapping;
        MatrixDataType(int mapping) {
            this.mapping = mapping;
        }
    }
}

Надеюсь, это поможет,

РЕДАКТИРОВАТЬ:

Извините, но я не нашел надежных способов:

  • Выберите стиль массива. Кажется, это открытый вопрос о Jackson Github
  • Что касается ярлыков YAML, я не нашел никакой подсказки по этому поводу. Возможно, вам придется заполнить вопрос о Джексоне, если вы решите придерживаться его. После поиска способа кодирования

EDIT 2: Я модифицировал приведенный выше пример кода для сериализации / десериализации объектов Mat. Однако я использовал циклы, как вы это делали для заполнить / получить значения матрицы, поскольку я не смог найти способ передачи значений через ByteBuffers (и, поверьте мне, я пытался. Но ни Java API, ни документация не очень помогают в этом отношении).

Вы можете видеть, что введенный объект Matrix все еще существует, потому что он облегчает работу по конвертации ИМХО. Вы можете, если хотите, избавиться от него, если вы используете объект Jackson StdSerializer вместо Converter. Однако я Я не уверен, что это будет чище.

Последние слова:

  • Я нашел этот другой пост SO о матрице OpenCV при поиске передачи данных. Может быть, это будет вам полезно.
  • Осторожно, мой код правильно обрабатывает только 64-битные значения с плавающей запятой. Вам нужно будет начать отсюда, чтобы добавить другие случаи CvType, если это необходимо.

Ну, это т Я уверен, что больше не могу вам помочь. Удачи; -)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...