Refactor & test foxglove image converter

Add a simple test to confirm that it actually produces useful images.

Trying to get a start on better testing for the infrastructure
associated with various calibration stuff, and figured I'd actually add
tests for the code that I added...

Change-Id: I4ac485b1c199f5412c9c6f41a9dd93639ce8f1a1
Signed-off-by: James Kuszmaul <jabukuszmaul@gmail.com>
diff --git a/WORKSPACE b/WORKSPACE
index 3e1801c..aa92b7e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -549,6 +549,18 @@
     actual = "@com_google_googletest//:gtest_main",
 )
 
+http_archive(
+    name = "april_tag_test_image",
+    build_file_content = """
+filegroup(
+    name = "april_tag_test_image",
+    srcs = ["test.bfbs", "expected.jpeg", "expected.png"],
+    visibility = ["//visibility:public"],
+)""",
+    sha256 = "5312c79b19e9883b3cebd9d65b4438a2bf05b41da0bcd8c35e19d22c3b2e1859",
+    urls = ["https://www.frc971.org/Build-Dependencies/test_image_frc971.vision.CameraImage_2023.01.28.tar.gz"],
+)
+
 # Recompressed from libusb-1.0.21.7z.
 http_file(
     name = "libusb_1_0_windows",
diff --git a/frc971/vision/BUILD b/frc971/vision/BUILD
index 308792c..5d20aff 100644
--- a/frc971/vision/BUILD
+++ b/frc971/vision/BUILD
@@ -1,4 +1,5 @@
 load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_py_library")
+load("//aos:config.bzl", "aos_config")
 load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
 
 flatbuffer_cc_library(
@@ -262,9 +263,42 @@
     hdrs = ["foxglove_image_converter.h"],
     visibility = ["//visibility:public"],
     deps = [
+        ":charuco_lib",
         ":vision_fbs",
         "//aos/events:event_loop",
         "//third_party:opencv",
         "@com_github_foxglove_schemas//:schemas",
     ],
 )
+
+aos_config(
+    name = "converter_config",
+    testonly = True,
+    src = "converter_test_config.json",
+    flatbuffers = [
+        "//frc971/vision:vision_fbs",
+        "//aos/events:event_loop_fbs",
+        "//aos/logging:log_message_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:timestamp_fbs",
+        "@com_github_foxglove_schemas//:schemas",
+    ],
+)
+
+cc_test(
+    name = "foxglove_image_converter_test",
+    srcs = ["foxglove_image_converter_test.cc"],
+    data = [
+        ":converter_config",
+        "@april_tag_test_image",
+    ],
+    deps = [
+        ":foxglove_image_converter",
+        "//aos:configuration",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//aos/testing:path",
+        "//aos/testing:tmpdir",
+    ],
+)
diff --git a/frc971/vision/converter_test_config.json b/frc971/vision/converter_test_config.json
new file mode 100644
index 0000000..5d74dd1
--- /dev/null
+++ b/frc971/vision/converter_test_config.json
@@ -0,0 +1,46 @@
+{
+  "channels" : [
+    {
+      "name": "/aos",
+      "type": "aos.timing.Report",
+      "source_node": "test"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "test"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "test"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "test"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "test"
+    },
+    {
+      "name": "/camera",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "test",
+      "max_size": 10000000
+    },
+    {
+      "name": "/visualize",
+      "type": "foxglove.CompressedImage",
+      "source_node": "test",
+      "max_size": 10000000
+    }
+  ],
+  "nodes": [
+    {
+      "name": "test"
+    }
+  ]
+}
diff --git a/frc971/vision/foxglove_image_converter.cc b/frc971/vision/foxglove_image_converter.cc
index 0c5c736..abde78b 100644
--- a/frc971/vision/foxglove_image_converter.cc
+++ b/frc971/vision/foxglove_image_converter.cc
@@ -3,7 +3,6 @@
 #include <opencv2/imgproc.hpp>
 
 namespace frc971::vision {
-namespace {
 std::string_view ExtensionForCompression(ImageCompression compression) {
   switch (compression) {
     case ImageCompression::kJpeg:
@@ -12,25 +11,18 @@
       return "png";
   }
 }
-}  // namespace
+
 flatbuffers::Offset<foxglove::CompressedImage> CompressImage(
-    const CameraImage *raw_image, flatbuffers::FlatBufferBuilder *fbb,
-    ImageCompression compression) {
+    const cv::Mat image, const aos::monotonic_clock::time_point eof,
+    flatbuffers::FlatBufferBuilder *fbb, ImageCompression compression) {
   std::string_view format = ExtensionForCompression(compression);
   // imencode doesn't let us pass in anything other than an std::vector, and
   // performance isn't yet a big enough issue to try to avoid the copy.
   std::vector<uint8_t> buffer;
-  CHECK(raw_image->has_data());
-  cv::Mat image_color_mat(cv::Size(raw_image->cols(), raw_image->rows()),
-                          CV_8UC2, (void *)raw_image->data()->data());
-  cv::Mat bgr_image(cv::Size(raw_image->cols(), raw_image->rows()), CV_8UC3);
-  cv::cvtColor(image_color_mat, bgr_image, cv::COLOR_YUV2BGR_YUYV);
-  CHECK(cv::imencode(absl::StrCat(".", format), bgr_image, buffer));
+  CHECK(cv::imencode(absl::StrCat(".", format), image, buffer));
   const flatbuffers::Offset<flatbuffers::Vector<uint8_t>> data_offset =
       fbb->CreateVector(buffer);
-  const struct timespec timestamp_t =
-      aos::time::to_timespec(aos::monotonic_clock::time_point(
-          std::chrono::nanoseconds(raw_image->monotonic_timestamp_ns())));
+  const struct timespec timestamp_t = aos::time::to_timespec(eof);
   const foxglove::Time time{static_cast<uint32_t>(timestamp_t.tv_sec),
                             static_cast<uint32_t>(timestamp_t.tv_nsec)};
   const flatbuffers::Offset<flatbuffers::String> format_offset =
@@ -47,13 +39,14 @@
                                                std::string_view output_channel,
                                                ImageCompression compression)
     : event_loop_(event_loop),
+      image_callback_(
+          event_loop_, input_channel,
+          [this, compression](const cv::Mat image,
+                              const aos::monotonic_clock::time_point eof) {
+            auto builder = sender_.MakeBuilder();
+            builder.CheckOk(builder.Send(
+                CompressImage(image, eof, builder.fbb(), compression)));
+          }),
       sender_(
-          event_loop_->MakeSender<foxglove::CompressedImage>(output_channel)) {
-  event_loop_->MakeWatcher(input_channel, [this, compression](
-                                              const CameraImage &image) {
-    auto builder = sender_.MakeBuilder();
-    builder.CheckOk(
-        builder.Send(CompressImage(&image, builder.fbb(), compression)));
-  });
-}
+          event_loop_->MakeSender<foxglove::CompressedImage>(output_channel)) {}
 }  // namespace frc971::vision
diff --git a/frc971/vision/foxglove_image_converter.h b/frc971/vision/foxglove_image_converter.h
index add83a6..872ac14 100644
--- a/frc971/vision/foxglove_image_converter.h
+++ b/frc971/vision/foxglove_image_converter.h
@@ -1,8 +1,9 @@
 #ifndef FRC971_VISION_FOXGLOVE_IMAGE_CONVERTER_H_
 #define FRC971_VISION_FOXGLOVE_IMAGE_CONVERTER_H_
-#include "external/com_github_foxglove_schemas/CompressedImage_generated.h"
-#include "frc971/vision/vision_generated.h"
 #include "aos/events/event_loop.h"
+#include "external/com_github_foxglove_schemas/CompressedImage_generated.h"
+#include "frc971/vision/charuco_lib.h"
+#include "frc971/vision/vision_generated.h"
 
 namespace frc971::vision {
 // Empirically, from 2022 logs:
@@ -12,9 +13,11 @@
 // conversion with a user-script in Foxglove Studio.
 enum class ImageCompression { kJpeg, kPng };
 
+std::string_view ExtensionForCompression(ImageCompression compression);
+
 flatbuffers::Offset<foxglove::CompressedImage> CompressImage(
-    const CameraImage *raw_image, flatbuffers::FlatBufferBuilder *fbb,
-    ImageCompression compression);
+    const cv::Mat image, const aos::monotonic_clock::time_point eof,
+    flatbuffers::FlatBufferBuilder *fbb, ImageCompression compression);
 
 // This class provides a simple converter that will take an AOS CameraImage
 // channel and output
@@ -30,6 +33,7 @@
 
  private:
   aos::EventLoop *event_loop_;
+  ImageCallback image_callback_;
   aos::Sender<foxglove::CompressedImage> sender_;
 };
 }  // namespace frc971::vision
diff --git a/frc971/vision/foxglove_image_converter_test.cc b/frc971/vision/foxglove_image_converter_test.cc
new file mode 100644
index 0000000..65b3b6b
--- /dev/null
+++ b/frc971/vision/foxglove_image_converter_test.cc
@@ -0,0 +1,68 @@
+#include "frc971/vision/foxglove_image_converter.h"
+
+#include "aos/events/simulated_event_loop.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/testing/path.h"
+#include "aos/testing/tmpdir.h"
+#include "gtest/gtest.h"
+
+namespace frc971::vision {
+std::ostream &operator<<(std::ostream &os, ImageCompression compression) {
+  os << ExtensionForCompression(compression);
+  return os;
+}
+namespace testing {
+class ImageConverterTest : public ::testing::TestWithParam<ImageCompression> {
+ protected:
+  ImageConverterTest()
+      : config_(aos::configuration::ReadConfig(
+            aos::testing::ArtifactPath("frc971/vision/converter_config.json"))),
+        factory_(&config_.message()),
+        camera_image_(
+            aos::FileToFlatbuffer<CameraImage>(aos::testing::ArtifactPath(
+                "external/april_tag_test_image/test.bfbs"))),
+        node_(aos::configuration::GetNode(&config_.message(), "test")),
+        test_event_loop_(factory_.MakeEventLoop("test", node_)),
+        image_sender_(test_event_loop_->MakeSender<CameraImage>("/camera")),
+        converter_event_loop_(factory_.MakeEventLoop("converter", node_)),
+        converter_(converter_event_loop_.get(), "/camera", "/visualize",
+                   GetParam()),
+        output_path_(absl::StrCat(aos::testing::TestTmpDir(), "/test.",
+                                  ExtensionForCompression(GetParam()))) {
+    test_event_loop_->OnRun(
+        [this]() { image_sender_.CheckOk(image_sender_.Send(camera_image_)); });
+    test_event_loop_->MakeWatcher(
+        "/visualize", [this](const foxglove::CompressedImage &image) {
+          ASSERT_TRUE(image.has_data());
+          std::string expected_contents =
+              aos::util::ReadFileToStringOrDie(aos::testing::ArtifactPath(
+                  absl::StrCat("external/april_tag_test_image/expected.",
+                               ExtensionForCompression(GetParam()))));
+          std::string_view data(
+              reinterpret_cast<const char *>(image.data()->data()),
+              image.data()->size());
+          EXPECT_EQ(expected_contents, data);
+          aos::util::WriteStringToFileOrDie(output_path_, data);
+          factory_.Exit();
+        });
+  }
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory factory_;
+  aos::FlatbufferVector<CameraImage> camera_image_;
+  const aos::Node *const node_;
+  std::unique_ptr<aos::EventLoop> test_event_loop_;
+  aos::Sender<CameraImage> image_sender_;
+  std::unique_ptr<aos::EventLoop> converter_event_loop_;
+  FoxgloveImageConverter converter_;
+  std::string output_path_;
+};
+
+TEST_P(ImageConverterTest, ImageToFoxglove) { factory_.Run(); }
+
+INSTANTIATE_TEST_SUITE_P(CompressionOptions, ImageConverterTest,
+                         ::testing::Values(ImageCompression::kJpeg,
+                                           ImageCompression::kPng));
+
+}  // namespace testing
+}  // namespace frc971::vision