rpcs3/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp
Megamouse ec7f50d925 ps move tracker: set max of radii to 50%
It makes no sense to have a larger radius than half of the image size
2024-12-21 14:36:24 +01:00

604 lines
18 KiB
C++

#include "stdafx.h"
#include "ps_move_tracker_dialog.h"
#include "ui_ps_move_tracker_dialog.h"
#include "Emu/Cell/Modules/cellCamera.h"
#include "qt_camera_handler.h"
#include "Input/ps_move_handler.h"
#include "Input/ps_move_config.h"
#include "Input/ps_move_tracker.h"
#include <QCheckBox>
#include <QComboBox>
#include <QImage>
#include <QPainter>
#include <QPushButton>
#include <QSlider>
#include <QMessageBox>
LOG_CHANNEL(ps_move);
extern u32 get_buffer_size_by_format(s32, s32, s32);
static constexpr bool tie_hue_to_color = true;
static constexpr int radius_range = 1000;
static const constexpr f64 min_radius_conversion = radius_range / g_cfg_move.min_radius.max;
static const constexpr f64 max_radius_conversion = radius_range / g_cfg_move.max_radius.max;
extern void qt_events_aware_op(int repeat_duration_ms, std::function<bool()> wrapped_op);
ps_move_tracker_dialog::ps_move_tracker_dialog(QWidget* parent)
: QDialog(parent)
, ui(new Ui::ps_move_tracker_dialog)
{
ui->setupUi(this);
if (!g_cfg_move.load())
{
ps_move.notice("Could not load PS Move config. Using defaults.");
}
setAttribute(Qt::WA_DeleteOnClose);
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton* button)
{
if (button == ui->buttonBox->button(QDialogButtonBox::Save))
{
g_cfg_move.save();
}
else if (button == ui->buttonBox->button(QDialogButtonBox::Apply))
{
g_cfg_move.save();
}
else if (button == ui->buttonBox->button(QDialogButtonBox::Close))
{
if (!g_cfg_move.load())
{
ps_move.notice("Could not load PS Move config. Using defaults.");
}
}
});
m_format = CELL_CAMERA_RGBA;
ui->inputFormatCombo->addItem(tr("RGBA"), static_cast<int>(CELL_CAMERA_RGBA));
ui->inputFormatCombo->addItem(tr("RAW8"), static_cast<int>(CELL_CAMERA_RAW8));
ui->inputFormatCombo->setCurrentIndex(ui->inputFormatCombo->findData(m_format));
connect(ui->inputFormatCombo, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->inputFormatCombo->currentData(); qvar.canConvert<int>())
{
m_format = qvar.toInt();
reset_camera();
}
});
ui->viewCombo->addItem(tr("Image"), static_cast<int>(view_mode::image));
ui->viewCombo->addItem(tr("Grayscale"), static_cast<int>(view_mode::grayscale));
ui->viewCombo->addItem(tr("HSV Hue"), static_cast<int>(view_mode::hsv_hue));
ui->viewCombo->addItem(tr("HSV Saturation"), static_cast<int>(view_mode::hsv_saturation));
ui->viewCombo->addItem(tr("HSV Value"), static_cast<int>(view_mode::hsv_value));
ui->viewCombo->addItem(tr("Binary"), static_cast<int>(view_mode::binary));
ui->viewCombo->addItem(tr("Contours"), static_cast<int>(view_mode::contours));
ui->viewCombo->setCurrentIndex(ui->viewCombo->findData(static_cast<int>(m_view_mode)));
connect(ui->viewCombo, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->viewCombo->currentData(); qvar.canConvert<int>())
{
m_view_mode = static_cast<view_mode>(qvar.toInt());
}
});
ui->histoCombo->addItem(tr("Hues"), static_cast<int>(histo_mode::unfiltered_hues));
ui->histoCombo->setCurrentIndex(ui->viewCombo->findData(static_cast<int>(m_histo_mode)));
connect(ui->histoCombo, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->histoCombo->currentData(); qvar.canConvert<int>())
{
m_histo_mode = static_cast<histo_mode>(qvar.toInt());
}
});
connect(ui->hueSlider, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 hue = std::clamp<u16>(value, config->hue.min, config->hue.max);
config->hue.set(hue);
update_hue();
});
ui->hueSlider->setRange(g_cfg_move.move1.hue.min, g_cfg_move.move1.hue.max);
connect(ui->hueThresholdSlider, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 hue_threshold = std::clamp<u16>(value, config->hue_threshold.min, config->hue_threshold.max);
config->hue_threshold.set(hue_threshold);
update_hue_threshold();
});
ui->hueThresholdSlider->setRange(g_cfg_move.move1.hue_threshold.min, g_cfg_move.move1.hue_threshold.max);
connect(ui->saturationThresholdSlider, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 saturation_threshold = std::clamp<u16>(value, config->saturation_threshold.min, config->saturation_threshold.max);
config->saturation_threshold.set(saturation_threshold);
update_saturation_threshold();
});
ui->saturationThresholdSlider->setRange(g_cfg_move.move1.saturation_threshold.min, g_cfg_move.move1.saturation_threshold.max);
connect(ui->minRadiusSlider, &QSlider::valueChanged, this, [this](int value)
{
const f32 min_radius = std::clamp(value / min_radius_conversion, g_cfg_move.min_radius.min, g_cfg_move.min_radius.max);
g_cfg_move.min_radius.set(min_radius);
update_min_radius();
});
ui->minRadiusSlider->setRange(0, radius_range);
connect(ui->maxRadiusSlider, &QSlider::valueChanged, this, [this](int value)
{
const f32 max_radius = std::clamp(value / max_radius_conversion, g_cfg_move.max_radius.min, g_cfg_move.max_radius.max);
g_cfg_move.max_radius.set(max_radius);
update_max_radius();
});
ui->maxRadiusSlider->setRange(0, radius_range);
connect(ui->colorSliderR, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
config->r.set(std::clamp<u8>(value, config->r.min, config->r.max));
update_color();
});
connect(ui->colorSliderG, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
config->g.set(std::clamp<u8>(value, config->g.min, config->g.max));
update_color();
});
connect(ui->colorSliderB, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
config->b.set(std::clamp<u8>(value, config->b.min, config->b.max));
update_color();
});
ui->colorSliderR->setRange(g_cfg_move.move1.r.min, g_cfg_move.move1.r.max);
ui->colorSliderG->setRange(g_cfg_move.move1.g.min, g_cfg_move.move1.g.max);
ui->colorSliderB->setRange(g_cfg_move.move1.b.min, g_cfg_move.move1.b.max);
connect(ui->filterSmallContoursBox, &QCheckBox::toggled, [this](bool checked)
{
m_filter_small_contours = checked;
});
ui->filterSmallContoursBox->setChecked(m_filter_small_contours);
connect(ui->freezeFrameBox, &QCheckBox::toggled, [this](bool checked)
{
m_freeze_frame = checked;
});
ui->freezeFrameBox->setChecked(m_freeze_frame);
connect(ui->showAllContoursBox, &QCheckBox::toggled, [this](bool checked)
{
m_show_all_contours = checked;
});
ui->showAllContoursBox->setChecked(m_show_all_contours);
connect(ui->drawContoursBox, &QCheckBox::toggled, [this](bool checked)
{
m_draw_contours = checked;
});
ui->drawContoursBox->setChecked(m_draw_contours);
connect(ui->drawOverlaysBox, &QCheckBox::toggled, [this](bool checked)
{
m_draw_overlays = checked;
});
ui->drawOverlaysBox->setChecked(m_draw_overlays);
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
ui->comboSelectDevice->addItem(tr("PS Move #%0").arg(index + 1), index);
}
ui->comboSelectDevice->setCurrentIndex(ui->comboSelectDevice->findData(m_index));
connect(ui->comboSelectDevice, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->comboSelectDevice->currentData(); qvar.canConvert<int>())
{
m_index = qvar.toInt();
update_color(true);
update_hue(true);
update_hue_threshold(true);
update_saturation_threshold(true);
}
});
m_ps_move_tracker = std::make_unique<ps_move_tracker<true>>();
m_update_timer = new QTimer(this);
connect(m_update_timer, &QTimer::timeout, this, [this]()
{
std::lock_guard lock(m_image_mutex);
if (m_image.isNull() || m_histogram.isNull())
return;
ui->imageLabel->setPixmap(m_image);
ui->histogramLabel->setPixmap(m_histogram);
});
reset_camera();
m_input_thread = std::make_unique<named_thread<pad_thread>>(thread(), window(), "");
qt_events_aware_op(0, [](){ return !!pad::g_started; });
adjustSize();
update_color(true);
update_hue(true);
update_hue_threshold(true);
update_saturation_threshold(true);
update_min_radius(true);
update_max_radius(true);
if constexpr (!g_ps_move_tracking_supported)
{
QTimer::singleShot(1000, this, [this]()
{
QMessageBox::warning(this, QObject::tr("Tracking not supported!"), QObject::tr("The PS Move tracking is not yet supported on this operating system."));
});
}
}
ps_move_tracker_dialog::~ps_move_tracker_dialog()
{
m_update_timer->stop();
if (m_tracker_thread)
{
m_stop_threads = 1;
m_tracker_thread->wait();
}
if (m_camera_handler)
{
m_camera_handler->close_camera();
}
if (m_input_thread)
{
auto& thread = *m_input_thread;
thread = thread_state::aborting;
thread();
m_input_thread.reset();
}
}
void ps_move_tracker_dialog::update_color(bool update_sliders)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
ui->colorGb->setTitle(tr("Color: R=%0, G=%1, B=%2").arg(config->r.get()).arg(config->g.get()).arg(config->b.get()));
if (update_sliders)
{
ui->colorSliderR->setValue(config->r.get());
ui->colorSliderG->setValue(config->g.get());
ui->colorSliderB->setValue(config->b.get());
}
else if (tie_hue_to_color)
{
const auto [hue, saturation, value] = ps_move_tracker<true>::rgb_to_hsv(config->r.get() / 255.0f, config->g.get() / 255.0f, config->b.get() / 255.0f);
config->hue.set(std::clamp<u32>(hue, config->hue.min, config->hue.max));
update_hue(true);
}
if (!m_input_thread)
{
return;
}
std::lock_guard lock(pad::g_pad_mutex);
auto& handlers = m_input_thread->get_handlers();
if (auto it = handlers.find(pad_handler::move); it != handlers.end())
{
for (auto& binding : it->second->bindings())
{
if (binding.device)
{
binding.device->color_override_active = true;
binding.device->color_override.r = config->r.get();
binding.device->color_override.g = config->g.get();
binding.device->color_override.b = config->b.get();
}
}
}
}
void ps_move_tracker_dialog::update_hue(bool update_slider)
{
const u32 hue = ::at32(g_cfg_move.move, m_index)->hue.get();
ui->hueGb->setTitle(tr("Hue: %0").arg(hue));
if (update_slider)
{
ui->hueSlider->setValue(hue);
}
else if (tie_hue_to_color)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const auto [r, g, b] = ps_move_tracker<true>::hsv_to_rgb(hue, 1.0f, 1.0f);
config->r.set(r / 100);
config->g.set(g / 100);
config->b.set(b / 100);
update_color(true);
}
}
void ps_move_tracker_dialog::update_hue_threshold(bool update_slider)
{
const u32 hue_threshold = ::at32(g_cfg_move.move, m_index)->hue_threshold.get();
ui->hueThresholdGb->setTitle(tr("Hue Threshold: %0").arg(hue_threshold));
if (update_slider)
{
ui->hueThresholdSlider->setValue(hue_threshold);
}
}
void ps_move_tracker_dialog::update_saturation_threshold(bool update_slider)
{
const u32 saturation_threshold = ::at32(g_cfg_move.move, m_index)->saturation_threshold.get();
ui->saturationThresholdGb->setTitle(tr("Saturation Threshold: %0").arg(saturation_threshold));
if (update_slider)
{
ui->saturationThresholdSlider->setValue(saturation_threshold);
}
}
void ps_move_tracker_dialog::update_min_radius(bool update_slider)
{
ui->minRadiusGb->setTitle(tr("Min Radius: %0 %").arg(g_cfg_move.min_radius));
if (update_slider)
{
ui->minRadiusSlider->setValue(g_cfg_move.min_radius * min_radius_conversion);
}
}
void ps_move_tracker_dialog::update_max_radius(bool update_slider)
{
ui->maxRadiusGb->setTitle(tr("Max Radius: %0 %").arg(g_cfg_move.max_radius));
if (update_slider)
{
ui->maxRadiusSlider->setValue(g_cfg_move.max_radius * max_radius_conversion);
}
}
void ps_move_tracker_dialog::reset_camera()
{
m_update_timer->stop();
if (m_tracker_thread)
{
m_stop_threads = 1;
m_tracker_thread->wait();
m_stop_threads = 0;
}
std::lock_guard camera_lock(m_camera_handler_mutex);
const u64 size = get_buffer_size_by_format(m_format, width, height);
m_image_data_frozen.resize(size);
m_image_data.resize(size);
m_frame_number = 0;
m_camera_handler = std::make_unique<qt_camera_handler>();
m_camera_handler->set_resolution(width, height);
m_camera_handler->set_format(m_format, size);
m_camera_handler->set_mirrored(true);
m_camera_handler->open_camera();
m_camera_handler->start_camera();
m_update_timer->start(1000 / 60);
m_tracker_thread.reset(QThread::create([this]()
{
while (!m_stop_threads)
{
process_camera_frame();
}
}));
m_tracker_thread->start();
}
void ps_move_tracker_dialog::process_camera_frame()
{
std::lock_guard camera_lock(m_camera_handler_mutex);
if (!m_camera_handler || m_camera_handler->get_state() == qt_camera_handler::camera_handler_state::closed)
{
// Wait some time
std::this_thread::sleep_for(100us);
return;
}
u32 width = 0;
u32 height = 0;
u64 frame_number = 0;
u64 bytes_read = 0;
const camera_handler_base::camera_handler_state state = m_camera_handler->get_image(m_image_data.data(), m_image_data.size(), width, height, frame_number, bytes_read);
if (state != camera_handler_base::camera_handler_state::running || frame_number <= m_frame_number)
{
// Wait some time
std::this_thread::sleep_for(100us);
return;
}
m_frame_number = frame_number;
if (m_frame_frozen != m_freeze_frame)
{
m_frame_frozen = m_freeze_frame;
if (m_frame_frozen)
{
std::memcpy(m_image_data_frozen.data(), m_image_data.data(), m_image_data.size());
}
}
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
const cfg_ps_move* config = g_cfg_move.move[index];
m_ps_move_tracker->set_active(index, m_index == index);
m_ps_move_tracker->set_hue(index, config->hue);
m_ps_move_tracker->set_hue_threshold(index, config->hue_threshold);
m_ps_move_tracker->set_saturation_threshold(index, config->saturation_threshold);
}
m_ps_move_tracker->set_image_data(m_frame_frozen ? m_image_data_frozen.data() : m_image_data.data(), m_image_data.size(), width, height, m_camera_handler->format());
m_ps_move_tracker->set_min_radius(static_cast<f32>(g_cfg_move.min_radius) / 100.0f);
m_ps_move_tracker->set_max_radius(static_cast<f32>(g_cfg_move.max_radius) / 100.0f);
m_ps_move_tracker->set_filter_small_contours(m_filter_small_contours);
m_ps_move_tracker->set_show_all_contours(m_show_all_contours);
m_ps_move_tracker->set_draw_contours(m_draw_contours);
m_ps_move_tracker->set_draw_overlays(m_draw_overlays);
m_ps_move_tracker->process_image();
const std::vector<u8>* result = nullptr;
QImage::Format format = QImage::Format::Format_Invalid;
qsizetype bytes_per_line = 0;
switch (m_view_mode)
{
case view_mode::image:
{
result = &m_ps_move_tracker->rgba();
format = QImage::Format::Format_RGBA8888;
bytes_per_line = width * 4;
break;
}
case view_mode::grayscale:
{
result = &m_ps_move_tracker->gray();
format = QImage::Format::Format_Grayscale8;
bytes_per_line = width;
break;
}
case view_mode::hsv_hue:
case view_mode::hsv_saturation:
case view_mode::hsv_value:
{
const int index = static_cast<int>(m_view_mode) - static_cast<int>(view_mode::hsv_hue);
const std::vector<u8>& hsv = m_ps_move_tracker->hsv();
static std::vector<u8> hsv_single;
hsv_single.resize(hsv.size() / 3);
for (int i = 0; i < static_cast<int>(hsv_single.size()); i++)
{
hsv_single[i] = hsv[i * 3 + index];
}
result = &hsv_single;
format = QImage::Format::Format_Grayscale8;
bytes_per_line = width;
break;
}
case view_mode::binary:
{
result = &m_ps_move_tracker->binary(m_index);
format = QImage::Format::Format_Grayscale8;
bytes_per_line = width;
break;
}
case view_mode::contours:
{
result = &m_ps_move_tracker->rgba_contours();
format = QImage::Format::Format_RGBA8888;
bytes_per_line = width * 4;
break;
}
}
QPixmap histogram;
switch (m_histo_mode)
{
case histo_mode::unfiltered_hues:
{
histogram = get_histogram(m_ps_move_tracker->hues(), false);
break;
}
}
const QImage image(result->data(), width, height, bytes_per_line, format, nullptr, nullptr);
std::lock_guard lock(m_image_mutex);
m_image = QPixmap::fromImage(image);
m_histogram = std::move(histogram);
}
QPixmap ps_move_tracker_dialog::get_histogram(const std::array<u32, 360>& hues, bool ignore_zero) const
{
// Create image
const int height = ui->histogramLabel->height();
static QPixmap background = [&]()
{
// Paint background
QPixmap pxmap(static_cast<int>(hues.size()), height);
pxmap.fill(Qt::white);
return pxmap;
}();
QPixmap histo = background;
QPainter painter(&histo);
const cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 hue = config->hue;
const u16 hue_threshold = config->hue_threshold;
const int min_hue = hue - hue_threshold;
const int max_hue = hue + hue_threshold;
// Paint hue threshold
painter.fillRect(min_hue, 0, hue_threshold * 2 + 1, histo.height(), Qt::lightGray);
if (min_hue < 0)
{
painter.fillRect(min_hue + 360, 0, hue_threshold * 2 + 1, histo.height(), Qt::lightGray);
}
else if (max_hue >= 360)
{
painter.fillRect(0, 0, max_hue - 360, histo.height(), Qt::lightGray);
}
// Paint target hue
const auto [r, g, b] = ps_move_tracker<true>::hsv_to_rgb(hue, 1.0f, 1.0f);
painter.setPen(QColor(r, g, b));
painter.drawLine(hue, 0, hue, histo.height() - 1);
// Paint histogram
painter.setPen(Qt::black);
const u32 zero_offset = (ignore_zero ? 1 : 0);
const auto max_elem = std::max_element(hues.begin() + zero_offset, hues.end());
const u32 max_value = max_elem != hues.end() ? *max_elem : 0u;
if (!max_value)
return histo;
for (int i = zero_offset; i < static_cast<int>(hues.size()); i++)
{
const int bar_height = (hues[i] / static_cast<float>(max_value)) * height;
if (bar_height <= 0) continue;
painter.drawLine(i, height - 1, i, height - bar_height);
}
return histo;
}