diff options
| author | Nikita Kostovsky <nikita@kostovsky.me> | 2025-01-12 11:50:34 +0100 |
|---|---|---|
| committer | Nikita Kostovsky <nikita@kostovsky.me> | 2025-01-12 11:50:34 +0100 |
| commit | 9dde2ab53c8e2c97647164fce89cf149260fbc8f (patch) | |
| tree | f428169ce67a93d0532d91883e18892736bb26b4 /src | |
| parent | 8630381c7e1fa1527026b9c823790dc3f92c6321 (diff) | |
implement calibration
Diffstat (limited to 'src')
| -rw-r--r-- | src/calibration.cpp | 439 | ||||
| -rw-r--r-- | src/calibration.h | 31 | ||||
| -rw-r--r-- | src/constants.h | 23 | ||||
| -rw-r--r-- | src/dumps.cpp | 141 | ||||
| -rw-r--r-- | src/dumps.h | 6 | ||||
| -rw-r--r-- | src/pixels.cpp | 95 | ||||
| -rw-r--r-- | src/pixels.h | 32 | ||||
| -rw-r--r-- | src/typedefs.h | 33 |
8 files changed, 800 insertions, 0 deletions
diff --git a/src/calibration.cpp b/src/calibration.cpp new file mode 100644 index 0000000..73bd786 --- /dev/null +++ b/src/calibration.cpp @@ -0,0 +1,439 @@ +#include "calibration.h" + +#include <execution> +#include <iostream> + +#include <QDebug> +#include <QDir> +#include <QFile> +#include <QImage> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> + +#include "imagealgos.h" + +bool openCalibrationTable(const QString& filename, CalibrationTablePtr& table) +{ + QFile f(filename); + + if (!f.open(QFile::ReadOnly)) { + qWarning() << Q_FUNC_INFO << "cannot open" << filename + << "for reading:" << f.errorString(); + + return false; + } + + table.reset(new CalibrationTable{{0}}); + auto bytes = f.read((char*) table.data(), sizeof(CalibrationTable)); + + if (bytes != sizeof(CalibrationTable)) { + qWarning() << "cannot read calibration table from" << filename << bytes; + + if (f.error()) { + qWarning() << f.errorString() << f.error() << (void*) table.data(); + } else { + qWarning() << "file size:" << f.size() << "; got:" << bytes; + } + + return false; + } + + // for (const auto& col : *table) { + // qDebug() << "calibration column mid:" << col[640]; + // } + + return true; +} + +bool dump(const CalibrationTablePtr& table, const QString& filename) +{ + qDebug() << Q_FUNC_INFO << "size is" << sizeof(CalibrationTable); + + QFile f(filename); + + if (!f.open(QFile::WriteOnly)) { + qWarning() << Q_FUNC_INFO << "cannot open" << filename + << "for writing:" << f.errorString(); + + return false; + } + + const auto written = f.write((const char*) table.data(), + sizeof(CalibrationTable)); + + if (written != sizeof(CalibrationTable) || !f.flush()) { + qWarning() << Q_FUNC_INFO << "cannot write" << filename << ":" + << f.errorString(); + return false; + } + + return true; +} + +void interpolate(CalibrationTablePtr& table) +{ + std::for_each(std::execution::par, + table->begin(), + table->end(), + [](auto& column) { interpolate(column); }); + + for (size_t i = 9471; i < 9472; i++) { + std::cout << "row #" << i << ": "; + + for (size_t j = 0; j < 1280; ++j) { + const auto& p = table->at(j).at(i); + std::cout << p << ' '; + } + + std::cout << std::endl; + } + + // for (size_t i = 0; i < discretesInRage; i++) { + // std::cout << "row #" << i << ": "; + + // for (size_t j = 640 - 5; j < 640 + 5; ++j) { + // const auto& p = table->at(j).at(i); + // std::cout << p << ' '; + // } + + // std::cout << std::endl; + // } + + // for (const auto& p : (*table)[table->size() / 2]) { + // qDebug() << "mid column pixel" << p; + // } +} + +void interpolate(CalibrationColumn& column) +{ + size_t start{0}; + auto& c = column; + +#define FIND_IF(index, condition) \ + while (bool(c[index]) != condition && index < calibrationTableHeight) \ + ++index; \ +\ + if (index == calibrationTableHeight) \ + return + + FIND_IF(start, true); + + while (true) { + size_t left = start + 1; + FIND_IF(left, false); + --left; + + size_t right = left + 1; + FIND_IF(right, true); + + auto delta = (c[right] - c[left]) / (right - left); + + for (auto i = left + 1; i < right; ++i) { + c[i] = c[i - 1] + delta; + + if (c[i] > 190.) { + qWarning() << "interpolate: got invalid value mm" << c[i]; + + qWarning() << "left/i/right" << left << i << right; + qWarning() << "delta" << delta; + qWarning() << "c[left/i/right]" << c[left] << c[i] << c[right]; + + exit(EXIT_FAILURE); + } + } + + start = right; + } +} + +QImage calibrationTableToImage(const CalibrationTablePtr& calibrationTable) +{ + QImage result(QSize(calibrationTable->size(), + calibrationTable->at(0).size()), + QImage::Format::Format_Indexed8); + + // QImage image(QSize(imageWidth, imageHeight), QImage::Format_Indexed8); + + QColor color(Qt::green); + auto r = color.redF(); + auto g = color.greenF(); + auto b = color.blueF(); + + for (int c = 0; c < 256; c++) { + QRgb col = qRgb(int(c * r), int(c * g), int(c * b)); + result.setColor(c, col); + } + + int notNull = 0; + + for (size_t colIdx = 0; colIdx < calibrationTable->size(); ++colIdx) { + const auto& column = calibrationTable->at(colIdx); + + for (size_t rowIdx = 0; rowIdx < column.size(); ++rowIdx) { + bool hasValue = !qFuzzyIsNull(column.at(rowIdx)); + + notNull += int(hasValue); + + result.setPixel(colIdx, rowIdx, hasValue ? 255 : 0); + // if (column.at(rowIdx) >= 190.) { + // qWarning() << "invalid mm value" << column.at(rowIdx); + // exit(EXIT_FAILURE); + // } + // result.setPixel(colIdx, rowIdx, (column.at(rowIdx) / 190.) * 255); + } + } + + qDebug() << "not null count" << notNull << "of" + << sizeof(CalibrationTable) + / sizeof(calibrationTable->at(0).at(0)); + + return result; +} + +CalibrationTablePtr calibrateZ(const QList<Pixels>& rawProfiles, + const uint32_t& stepsPerMm) +{ + CalibrationTablePtr result{new CalibrationTable{{0}}}; + + for (const auto& rawProfile : rawProfiles) { + // std::cout << "calibration: pos is " + // << rawProfile.counters.encoderPosition << std::endl; + const float positionMm{float(rawProfile.counters.encoderPosition) + / float(stepsPerMm)}; + + if (positionMm >= 200) { + qWarning() << "got invalid calibration distance:" << positionMm; + qWarning() << "encoder position:" + << rawProfile.counters.encoderPosition; + exit(EXIT_FAILURE); + } + + const auto& pixels = rawProfile.pixels; + // printPixels(pixels); + // static size_t counter { 0 }; + // qDebug() << "calibrated" << counter++; + const float pos = rawProfile.counters.encoderPosition; + const float divider = stepsPerMm; + // std::cout << pos << " " << divider << " " << pos / divider << + // std::endl; std::cout << std::setw(5) << + // rawProfile.counters.encoderPosition + // << std::setw(5) << requested_params.stepsPerMm + // << std::setw(8) << positionMm + // << std::setw(8) << + // float(rawProfile.counters.encoderPosition) / + // float(requested_params.stepsPerMm) + // << ": "; + + for (size_t columnIdx = 0; columnIdx < pixels.size(); ++columnIdx) { + const auto& pixelValue = pixels.at(columnIdx); + + // float[0, img_height] -> uint16_t[0, calibrationColumnHeight] + const uint16_t discretePixelValue{ + uint16_t(pixelValue * discretesInRage / img_height)}; + + Q_ASSERT_X(discretePixelValue > 0 + && discretePixelValue < calibrationColumnHeight, + Q_FUNC_INFO, + ("got ivalid discrete value " + + QString::number(discretePixelValue) + + ". pixelValues is " + QString::number(pixelValue) + + ". calc result is " + + QString::number(pixelValue * discretesInRage + / img_height)) + .toStdString() + .c_str()); + + // auto& calibrationColumn = result[columnIdx]; + auto& calibrationColumn = (*result)[columnIdx]; + + calibrationColumn[discretePixelValue] = positionMm; + + // if (columnIdx >= ((img_width - 10) / 2) && + // columnIdx < img_width - ((img_width - 10) / 2)) + // { + // std::cout << discretePixelValue << ";"; + // } + } + + // std::cout << std::endl << std::flush; + } + + return result; + // TODO: try something interesting + // return {}; +} + +CalibrationTablePtr calibrateX(const QList<Pixels>& rawProfiles) +{ + CalibrationTablePtr result{new CalibrationTable{{0}}}; + + static size_t tmp_counter = 0; + + for (const auto& rawProfile : rawProfiles) { + const auto& pixels = rawProfile.pixels; + auto lines = pixelsToLines(rawProfile); + + tmp_counter++; + + Q_ASSERT_X(lines.count() > 2, Q_FUNC_INFO, "no lines"); + + if (tmp_counter == 9471) { + for (const auto& l : lines) { + qDebug() << "line:" << l; + } + } + + QList<double> xAnchors(lines.size() + 1); + + std::transform(std::execution::par_unseq, + lines.constBegin(), + lines.constEnd(), + xAnchors.begin(), + [](const auto& l) { return l.x1(); }); + + xAnchors.last() = lines.last().x2(); + + constexpr double triangleHeightMm{5.}; + constexpr double triangleBaseMm{8.}; + + auto nearestAnchorToX0 = std::min_element(std::execution::par_unseq, + xAnchors.constBegin(), + xAnchors.constEnd(), + [](const auto& a, + const auto& b) { + return std::abs(a) + < std::abs(b); + }); + + int nearestAnchorToX0Idx = nearestAnchorToX0 - xAnchors.constBegin(); + + QList<double> xAnchorsMm(xAnchors.count()); + + for (int i = 0; i < xAnchors.size(); ++i) { + xAnchorsMm[i] = (i - nearestAnchorToX0Idx) * triangleBaseMm / 2.; + } + + auto xAnchorIt = xAnchors.constBegin() + 1; + auto xAnchorMmIt = xAnchorsMm.constBegin() + 1; + + for (size_t columnIdx = 0; columnIdx < pixels.size(); ++columnIdx) { + // skip points with to the left from left line and to the right from + // right line + const auto columnX = int(columnIdx) - int(img_width / 2); + if (columnX < xAnchors.first() || columnX > xAnchors.last()) { + continue; + } + + if (columnX > *xAnchorIt) { + ++xAnchorIt; + ++xAnchorMmIt; + } + const auto xLeft = *(xAnchorIt - 1); + const auto xRight = *(xAnchorIt); + + if (columnX < xLeft || columnX > xRight) { + if (rawProfile.counters.encoderPosition >= 0) { + qWarning() + << "x anchor not found" << xLeft << columnX << xRight; + continue; + } + } + + const auto& pixelValue = pixels.at(columnIdx); + const uint16_t discretePixelValue{ + uint16_t(pixelValue * discretesInRage / img_height)}; + + Q_ASSERT_X(discretePixelValue > 0 + && discretePixelValue < calibrationColumnHeight, + Q_FUNC_INFO, + ("got ivalid discrete value " + + QString::number(discretePixelValue) + + ". pixelValues is " + QString::number(pixelValue) + + ". calc result is " + + QString::number(pixelValue * discretesInRage + / img_height)) + .toStdString() + .c_str()); + + const auto xLineLen = xRight - xLeft; + const auto xLeftMm = *(xAnchorMmIt - 1); + const auto xRelative = float(columnX - xLeft) / xLineLen; + const auto xMmValue = xLeftMm + xRelative * (triangleBaseMm / 2.); + + auto& calibrationColumn = (*result)[columnIdx]; + + if (tmp_counter == 9471) { + qDebug() << "calibration value" << columnIdx + << discretePixelValue << xMmValue; + } + calibrationColumn[discretePixelValue] = xMmValue; + } + } + + return result; +} + +void dumpCalibrationPixels(std::vector<Pixels>&& calibrationPixels) +{ + std::vector<Pixels> rawProfiles; + + std::swap(rawProfiles, calibrationPixels); + + const QString dumpSubdir{ + QDateTime::currentDateTime().toString("yyyy.MM.dd_hh.mm.ss")}; + const QDir dumpPath{dumpsRoot + "/" + dumpSubdir}; + + if (!dumpPath.mkdir(dumpPath.path())) { + qWarning() << "cannot create dir: " << dumpPath.path(); + + return; + } + + for (const auto& rawProfile : rawProfiles) { + const auto filename = QLatin1String("raw_profile_meas_%1_enc_%2") + .arg(QString::number( + rawProfile.counters.measurementCounter)) + .arg(rawProfile.counters.encoderPosition); + const auto filepath = dumpPath.path() + "/" + filename; + + QFile f{filepath}; + + if (!f.open(QFile::WriteOnly)) { + qWarning() << "cannot open dump dump file" << f.fileName(); + qWarning() << "error is:" << f.errorString(); + + return; + } + + QJsonObject jsonCounters{ + {"timestampUs", qint64(rawProfile.counters.timestampUs)}, + {"measurementCounter", + qint64(rawProfile.counters.measurementCounter)}, + {"encoderPosition", qint64(rawProfile.counters.encoderPosition)}, + }; + + QJsonObject json; + + json["counters"] = jsonCounters; + + QJsonArray jsonPixels; + + for (const auto& pixel : rawProfile.pixels) { + jsonPixels << pixel; + } + + json["pixels"] = jsonPixels; + + if (!f.write(QJsonDocument(json).toJson())) { + qWarning() << "cannot write file" << f.fileName(); + qWarning() << "error is" << f.errorString(); + + return; + } + + qDebug() << "file written: " << f.fileName(); + } + + qDebug() << "dump finished"; +} diff --git a/src/calibration.h b/src/calibration.h new file mode 100644 index 0000000..ddb66a6 --- /dev/null +++ b/src/calibration.h @@ -0,0 +1,31 @@ +#pragma once + +#include <array> + +#include "fuck_intel.h" + +#include <QSharedPointer> + +#include "constants.h" +#include "pixels.h" + +using CalibrationColumn = std::array<float, calibrationTableHeight>; +// map [0; discretesInRage] (discretes) to [0; zRangeMm] (mm) +using CalibrationTable = std::array<CalibrationColumn, img_width>; +using CalibrationTablePtr = QSharedPointer<CalibrationTable>; + +constexpr auto calibrationColumnHeight = std::tuple_size<CalibrationColumn>(); + +bool openCalibrationTable(const QString &filename, CalibrationTablePtr &table); + +void dumpCalibrationPixels(std::vector<Pixels> &&calibrationPixels); +bool dump(const CalibrationTablePtr &table, const QString &filename); + +CalibrationTablePtr calibrateX(const QList<Pixels> &rawProfiles); +CalibrationTablePtr calibrateZ(const QList<Pixels> &rawProfiles, + const uint32_t &stepsPerMm); + +QImage calibrationTableToImage(const CalibrationTablePtr &calibrationTable); + +void interpolate(CalibrationTablePtr &table); +void interpolate(CalibrationColumn &column); diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 0000000..1c451ab --- /dev/null +++ b/src/constants.h @@ -0,0 +1,23 @@ +#pragma once + +#include <cstddef> +#include <cstdint> + +#include <QString> + +constexpr size_t img_width = 1280; +constexpr size_t img_height = 800; +constexpr uint32_t patternSize = 16; + +constexpr float hardcodedZRangeMm{175.f}; +constexpr size_t calibrationTableHeight{0x4000}; // 16384 + +namespace { +constexpr uint16_t discretesInRage{16384}; +} + +// http json keys +const QString exposureTimeKey = "exposureTime"; +const QString laserLevelKey = "laserLevel"; + +const QString dumpsRoot{QStringLiteral("/home/user/dumps")}; diff --git a/src/dumps.cpp b/src/dumps.cpp new file mode 100644 index 0000000..43a839c --- /dev/null +++ b/src/dumps.cpp @@ -0,0 +1,141 @@ +#include "dumps.h" + +#include <QDebug> +#include <QDir> +#include <QElapsedTimer> +#include <QFile> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> + +#include <iostream> + +QList<Pixels> openDump( + const QString &dumpPath) +{ + QString dirToRead{dumpPath}; + qDebug() << "trying to open dump path:" << dirToRead; + + if (dirToRead.isEmpty()) { + qDebug() << "dumpPath not specified. looking into" << dumpsRoot; + + QDir dumpsRootDir{dumpsRoot}; + + const auto filter = QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable; + // there is no battery in my rpi5 for now + const auto sort = QDir::Name; + const auto entries = dumpsRootDir.entryList(filter, sort); + + if (entries.isEmpty()) { + qWarning() << "dumps root" << dumpsRoot << "contains no dumps. " + << "specify existing dump path"; + + return {}; + } + + dirToRead = dumpsRoot + "/" + entries.last(); + } + + QDir dumpDir{dirToRead}; + + // const QStringList nameFilters{"*.bin"}; + const QStringList nameFilters{}; + const auto filter = QDir::Files; + const auto sort = QDir::Name; + + auto filenames = dumpDir.entryList(nameFilters, filter, sort); + + if (filenames.isEmpty()) { + qDebug() << "no filenames found in" << dumpDir.path(); + return {}; + } + + qDebug() << "create results array" << filenames.size(); + auto resultOptionals = QScopedPointer( + new QList<std::optional<Pixels>>(filenames.size())); + + QElapsedTimer t; + t.start(); + qDebug() << "open real files"; + std::cout << "here" << std::endl; + + std::transform( + std::execution::par_unseq, + filenames.begin(), + filenames.end(), + resultOptionals->begin(), + [dirToRead](const auto &filename) { + // std::cout << '.'; + // auto rawProfile = openRawProfile(dirToRead + "/" + filename); + auto rawProfile = Pixels::load(dirToRead + "/" + filename); + + return rawProfile; + }); + + filenames.clear(); + filenames.squeeze(); + + qDebug() << Q_FUNC_INFO << "open raw profiles: elapsed (ms)" << t.elapsed(); + // std::cout << std::endl; + + std::remove_if(std::execution::par, + resultOptionals->begin(), + resultOptionals->end(), + [](auto &a) { return !a.has_value(); }); + + QList<Pixels> result(resultOptionals->size()); + + std::transform(std::execution::par, + std::make_move_iterator(resultOptionals->begin()), + std::make_move_iterator(resultOptionals->end()), + result.begin(), + [](auto &p) { return p.value(); }); + + qDebug() << Q_FUNC_INFO << "elapsed (ms)" << t.elapsed(); + + return result; +} + +std::optional<Pixels> openRawProfile( + const QString &filePath) +{ + QFile f{filePath}; + + if (!f.open(QFile::ReadOnly)) { + qWarning() << "cannot open file for reading:" << f.fileName(); + qWarning() << "error string:" << f.errorString(); + + return {}; + } + + // TODO: rewrite to remove manual serialization/deserialization + const auto json = QJsonDocument::fromJson(f.readAll()).object(); + const auto jsonCounters = json["counters"].toObject(); + + Pixels result; + result.counters.timestampUs = jsonCounters["timestampUs"].toInteger(); + result.counters.measurementCounter = jsonCounters["measurementCounter"] + .toInteger(); + result.counters.encoderPosition = jsonCounters["encoderPosition"].toInteger(); + + const auto jsonPixels = json["pixels"].toArray(); + // TODO: check json pixels count +#ifdef FIRST_COLUMN_ONLY + result.pixels[0] = jsonPixels.at(0).toDouble(); +#else + std::transform( + // std::execution::par_unseq, + jsonPixels.constBegin(), + jsonPixels.constEnd(), + result.pixels.begin(), + [](const auto &jsonPixel) { return jsonPixel.toDouble(); }); + +// for (size_t i = 0; i < jsonPixels.count() && i < result.pixels.size(); +// ++i) +// { +// result.pixels[i] = jsonPixels[i].toDouble(); +// } +#endif + + return result; +} diff --git a/src/dumps.h b/src/dumps.h new file mode 100644 index 0000000..8aa8dc9 --- /dev/null +++ b/src/dumps.h @@ -0,0 +1,6 @@ +#pragma once + +#include "pixels.h" + +QList<Pixels> openDump(const QString &dumpPath = ""); +std::optional<Pixels> openRawProfile(const QString &filePath); diff --git a/src/pixels.cpp b/src/pixels.cpp new file mode 100644 index 0000000..b314e6a --- /dev/null +++ b/src/pixels.cpp @@ -0,0 +1,95 @@ +#include "pixels.h" + +#include <execution> +#include <filesystem> +#include <fstream> +#include <iostream> + +Pixels& Pixels::operator+=(const Pixels& other) +{ + std::transform( + std::execution::par, + pixels.begin(), + pixels.end(), + other.pixels.begin(), + pixels.begin(), + // [](auto& toAdd) { return dst += src; }); + std::plus<>() + ); + + return *this; +} + +Pixels& Pixels::operator/=(const float divider) +{ + std::for_each( + std::execution::par_unseq, + pixels.begin(), + pixels.end(), + [divider](auto& pixel) { pixel /= divider; } + ); + + return *this; +} + +std::optional<Pixels> Pixels::load(const QString& filename) +{ + const std::filesystem::path filepath{filename.toStdString()}; + + if (!std::filesystem::exists(filepath)) + { + std::cerr << "no such file: " << filepath << std::endl; + + return {}; + } + + std::ifstream ifs(filepath, std::ios::in | std::ios::binary); + + if (!ifs) + return {}; + + Pixels result; + ifs.read(reinterpret_cast<char*>(&result), sizeof(Pixels)); + ifs.close(); + + if (!ifs) + { + std::cerr << "cannot read " << filepath << std::endl; + + return {}; + } + + return result; +} + +bool Pixels::save(const QString& filename) +{ + const std::filesystem::path filepath{filename.toStdString()}; + const auto parent_path = filepath.parent_path(); + + if (!std::filesystem::exists(parent_path) && + !std::filesystem::create_directories(parent_path)) + { + std::cerr << "cannot create parent directory for file " << filepath + << std::endl; + + return false; + } + + std::ofstream ofs(filepath, std::ios::out | std::ios::binary); + + if (!ofs) + return false; + + ofs.write(reinterpret_cast<const char*>(this), sizeof(Pixels)); + ofs.close(); + + if (!ofs) + { + std::cerr << "cannot write " << filepath << std::endl; + + return false; + } + + return true; +} diff --git a/src/pixels.h b/src/pixels.h new file mode 100644 index 0000000..6095220 --- /dev/null +++ b/src/pixels.h @@ -0,0 +1,32 @@ +#pragma once + +#include <array> + +#include "fuck_intel.h" + +#include <QString> + +#include "typedefs.h" + +struct Pixels +{ + Counters counters{}; + std::array<float, img_width> pixels{0.f}; + + Pixels& operator+=(const Pixels& other); + Pixels& operator/=(const float divider); + + // TODO: tests for everything everywhere + /*! + * \brief load - load binary pixels from file + * \param filename - file path + * \return Pixels on success, empty std::optional otherwise + */ + [[nodiscard]] static std::optional<Pixels> load(const QString& filename); + /*! + * \brief save - save binary profile to file + * \param filename - file path + * \return - true on success, false otherwise + */ + [[nodiscard]] bool save(const QString& filename); +}; diff --git a/src/typedefs.h b/src/typedefs.h new file mode 100644 index 0000000..2af2921 --- /dev/null +++ b/src/typedefs.h @@ -0,0 +1,33 @@ +#pragma once + +#include <libcamera/pixel_format.h> + +#include "constants.h" + +struct Counters +{ + uint32_t timestampUs{0}; + uint32_t measurementCounter{0}; + int32_t encoderPosition{0}; +}; + +struct Image +{ + int width; + int height; + uint16_t data[img_height][img_width]; + uint16_t rotated_cw[img_width][img_height]; + size_t dataSize; + unsigned int stride; + libcamera::PixelFormat pixelFormat; + Counters counters{}; +}; + +struct requested_params_t +{ + int32_t exposureTime = {1000}; + int32_t laserLevel = {3000}; + uint32_t stepsPerMm{200}; +}; + +enum ScanningModeFlags : uint8_t { None = 0, Calibration }; |
