Repository: sz3/libcimbar
File: src/lib/cimb_translator/CimbReader.cpp (C++)

Code before:
namespace {
	cv::Mat kernel()
	{
		static const cv::Mat k = (cv::Mat_<float>(3,3) <<  -0, -1, -0, -1, 4.5, -1, -0, -1, -0);
		return k;
	}

	template <typename MAT>
	void sharpenSymbolGrid(const MAT& img, cv::Mat& out)
	{
		cv::filter2D(img, out, -1, kernel());
	}

	template <typename MAT>
	bitbuffer preprocessSymbolGrid(const MAT& img, bool needs_sharpen)
	{
		int blockSize = 3; // default: no preprocessing

		cv::Mat symbols;
		cv::cvtColor(img, symbols, cv::COLOR_RGB2GRAY);
		if (needs_sharpen)
			sharpenSymbolGrid(symbols, symbols); // we used to change blockSize for this case -- may one day be a useful trick again?
		cv::adaptiveThreshold(symbols, symbols, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, blockSize, -5);

		bitbuffer bb(std::pow(Config::image_size(), 2) / 8);
		bitmatrix::mat_to_bitbuffer(symbols, bb.get_writer());
		return bb;
	}

	void updateMaxColor(std::tuple<float, float, float>& max_color, const cv::Scalar& c)
	{
		std::get<0>(max_color) = std::max(std::get<0>(max_color), static_cast<float>(c[0]));
		std::get<1>(max_color) = std::max(std::get<1>(max_color), static_cast<float>(c[1]));
		std::get<2>(max_color) = std::max(std::get<2>(max_color), static_cast<float>(c[2]));
	}

	std::tuple<float, float, float> calculateWhite(const cv::Mat& img, bool dark)
	{
		std::tuple<float, float, float> bestColor({1, 1, 1});
		if (dark)
		{
			unsigned tl = Config::anchor_size() - 2;
			unsigned br = Config::image_size() - Config::anchor_size() - 2;
			std::array<std::pair<unsigned, unsigned>, 3> anchors = {{ {tl, tl}, {tl, br}, {br, tl} }};
			for (auto [x, y] : anchors)
			{
				cv::Rect crop(x, y, 4, 4);
				cv::Scalar avgColor = cv::mean(img(crop));
				updateMaxColor(bestColor, avgColor);
			}
		}

Review comment (suggestion):
It's not clear the best way to calculate it (do you include good and bad images? borderline ones? etc etc), but the most sensible way I have of evaluating the error rate introduced by that `-5` is to check how many of the sample images fit into certain error cutoffs.

Before and after, using blockSize=3 and different constant values (top=-5, bottom=0):

| <50 tile errors  | < 100 | < 200 | < 400 | < 500 | <700 | < 1000 | total |
| ---- | ----- | ----- | ----- | ----- | ---- | ------ | ----- |
| 28   | 62    | 98    | 149   | 168   | 190  | 223    |  371  |
| 107  | 141   | 177   | 208   | 226   | 251  | 266    |  371  |

Higher numbers are better (we could also express it as a %, e.g. 141/371 == `38%`), and includes all numbers to the left.

For very low error rates the difference between before and after is deceptive and mostly irrelevant (the ECC will correct 1 error or 60 with similar ease), but on the whole this means that the previous change was a significant regression under certain lighting conditions -- the 371 image dataset includes various lighting scenarios -- and fine under others.

(misc note: "tile errors" nor "bit errors" is exactly the metric we care about -- ideally we'd want to chart *byte* errors, since that's what reedsolomon corrects for. But tile errors is convenient to calculate, and operates as a proxy for byte errors. tile == 6 bits)

Code after:
namespace {
	cv::Mat kernel()
	{
		static const cv::Mat k = (cv::Mat_<float>(3,3) <<  -0, -1, -0, -1, 4.5, -1, -0, -1, -0);
		return k;
	}

	template <typename MAT>
	void sharpenSymbolGrid(const MAT& img, cv::Mat& out)
	{
		cv::filter2D(img, out, -1, kernel());
	}

	template <typename MAT>
	bitbuffer preprocessSymbolGrid(const MAT& img, bool needs_sharpen)
	{
		int blockSize = 5; // default: no preprocessing

		cv::Mat symbols;
		cv::cvtColor(img, symbols, cv::COLOR_RGB2GRAY);
		if (needs_sharpen)
		{
			blockSize = 7;
			sharpenSymbolGrid(symbols, symbols);
		}
		cv::adaptiveThreshold(symbols, symbols, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, blockSize, 0);

		bitbuffer bb(std::pow(Config::image_size(), 2) / 8);
		bitmatrix::mat_to_bitbuffer(symbols, bb.get_writer());
		return bb;
	}

	void updateMaxColor(std::tuple<float, float, float>& max_color, const cv::Scalar& c)
	{
		std::get<0>(max_color) = std::max(std::get<0>(max_color), static_cast<float>(c[0]));
		std::get<1>(max_color) = std::max(std::get<1>(max_color), static_cast<float>(c[1]));
		std::get<2>(max_color) = std::max(std::get<2>(max_color), static_cast<float>(c[2]));
	}

	std::tuple<float, float, float> calculateWhite(const cv::Mat& img, bool dark)
	{
		std::tuple<float, float, float> bestColor({1, 1, 1});
		if (dark)
		{
			unsigned tl = Config::anchor_size() - 2;
			unsigned br = Config::image_size() - Config::anchor_size() - 2;
			std::array<std::pair<unsigned, unsigned>, 3> anchors = {{ {tl, tl}, {tl, br}, {br, tl} }};
			for (auto [x, y] : anchors)
			{
				cv::Rect crop(x, y, 4, 4);
				cv::Scalar avgColor = cv::mean(img(crop));