diff --git a/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini b/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini
index a80598cec3a452..5b9c290023c601 100644
--- a/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini
+++ b/UI/frontend-plugins/frontend-tools/data/locale/en-US.ini
@@ -52,15 +52,48 @@ ScriptDescriptionLink.OpenURL="Open URL"
FileFilter.ScriptFiles="Script Files"
FileFilter.AllFiles="All Files"
+# Region of Interest Editor
+ROIEditor="Region of Interest Editor"
+
ROI.BlockSize="Encoder Block Size"
ROI.BlockSize.16="16x16 (H.264)"
ROI.BlockSize.32="32x32 (NVENC/QSV HEVC)"
ROI.BlockSize.64="64x64 (NVENC/QSV/AMF AV1, AMF HEVC)"
ROI.BlockSize.128="128x128 (AV1)"
-ROI.ManualRegion="Region [%1x%2 @ (%3, %4)]"
-ROI.SceneItem="Item [%1 (%2)]"
+ROI.Preview="ROI Encoder Map Preview"
+
+ROI.Scene="Scene:"
+
+ROI.ItemAction.Add="Add Item"
+ROI.ItemAction.Remove="Remove Item"
+ROI.ItemAction.MoveUp="Move Up"
+ROI.ItemAction.MoveDown="Move Down"
+
+ROI.AddMenu="Add Region"
+ROI.AddMenu.Manual="Manual Region"
+ROI.AddMenu.SceneItem="Scene Item Region"
+ROI.AddMenu.CenterFocus="Center Focus Region"
+
+ROI.Item.DisabledPrefix="DISABLED"
+ROI.Item.ManualRegion="Manual Region [%1x%2 @ (%3, %4)]"
+ROI.Item.SceneItem="Scene Item [%1 (%2)]"
+ROI.Item.CenterFocus="Center Focus"
-ROI.HelpText="The region of interest determines which areas an encoder should focus.
Note that not all encoders support this feature an some may overshoot the target bitrate when using ROI."
+ROI.Properties="Region Properties"
+ROI.Property.SceneItem="Scene Item"
+ROI.Property.SceneItem.NoSelection=""
+ROI.Property.Size="Size"
+ROI.Property.Position="Position"
+ROI.Property.Priority="Priority"
+ROI.Property.Enabled="Enabled"
+ROI.Property.OuterPriority="Outer Priority"
+ROI.Property.Steps="Steps"
+ROI.Property.Steps.Inner="Inner"
+ROI.Property.Steps.Outer="Outer"
+ROI.Property.RadiusInner="Inner Radius"
+ROI.Property.RadiusOuter="Outer Radius"
+ROI.Property.RadiusAspect="Stretch based on aspect ratio"
+ROI.HelpText="The region of interest determines which areas an encoder should (de-)prioritize.
Note that not all encoders support this feature an some may overshoot the target bitrate when using ROI."
diff --git a/UI/frontend-plugins/frontend-tools/forms/roi-editor.ui b/UI/frontend-plugins/frontend-tools/forms/roi-editor.ui
index 0b8d0abfb49a0d..90930dd4fe820a 100644
--- a/UI/frontend-plugins/frontend-tools/forms/roi-editor.ui
+++ b/UI/frontend-plugins/frontend-tools/forms/roi-editor.ui
@@ -29,7 +29,7 @@
- Scene
+ ROI.Scene
sceneSelect
@@ -78,7 +78,7 @@
-
- SceneItem
+ ROI.Property.SceneItem
roiPropSceneItem
@@ -88,14 +88,14 @@
-
- NoItem
+ ROI.Property.SceneItem.NoSelection
-
- Size
+ ROI.Property.Size
roiPropSizeX
@@ -135,7 +135,7 @@
-
- Position
+ ROI.Property.Position
roiPropPosX
@@ -167,31 +167,196 @@
-
-
+
- Priority
+ ROI.Property.Priority
- prioritySlider
+ roiPropPrioritySlider
-
-
-
- -100
+
+
-
+
+
+ -100
+
+
+ 100
+
+
+ true
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSlider::TickPosition::TicksBelow
+
+
+
+ -
+
+
+ %
+
+
+ -100
+
+
+ 100
+
+
+
+
+
+ -
+
+
+ ROI.Property.OuterPriority
-
- 100
+
+
+ -
+
+
-
+
+
+ -100
+
+
+ 100
+
+
+ true
+
+
+ Qt::Orientation::Horizontal
+
+
+ QSlider::TickPosition::TicksBelow
+
+
+
+ -
+
+
+ %
+
+
+ -100
+
+
+ 100
+
+
+
+
+
+ -
+
+
+ ROI.Property.RadiusInner
-
- true
+
+
+ -
+
+
-
+
+
+ 16384
+
+
+
+ -
+
+
+ ROI.Property.RadiusAspect
+
+
+
+
+
+ -
+
+
+ ROI.Property.RadiusOuter
+
+
+
+ -
+
+
-
+
+
+ 16384
+
+
+
+ -
+
+
+ ROI.Property.RadiusAspect
+
+
+
+
+
+ -
+
+
+ ROI.Property.Steps
-
- Qt::Orientation::Horizontal
+
+
+ -
+
+
-
+
+
+ ROI.Property.Steps.Inner
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+
+ -
+
+
+ ROI.Property.Steps.Outer
+
+
+
+ -
+
+
+ 1
+
+
+ 100
+
+
+
+
+
+ -
+
+
+ ROI.Property.Enabled
-
- QSlider::TickPosition::TicksBelow
+
+ true
@@ -232,6 +397,19 @@
+ -
+
+
+
+ 12
+ true
+
+
+
+ ROI.Preview
+
+
+
-
@@ -302,7 +480,7 @@
:/res/images/plus.svg:/res/images/plus.svg
- Add
+ ROI.ItemAction.Add
addIconSmall
@@ -314,7 +492,7 @@
:/res/images/minus.svg:/res/images/minus.svg
- Remove
+ ROI.ItemAction.Remove
Del
@@ -332,7 +510,7 @@
:/res/images/up.svg:/res/images/up.svg
- MoveUp
+ ROI.ItemAction.MoveUp
upArrowIconSmall
@@ -344,7 +522,7 @@
:/res/images/down.svg:/res/images/down.svg
- MoveDown
+ ROI.ItemAction.MoveDown
downArrowIconSmall
@@ -352,5 +530,70 @@
-
+
+
+ roiPropPrioritySlider
+ valueChanged(int)
+ roiPropPrioritySpinbox
+ setValue(int)
+
+
+ 349
+ 531
+
+
+ 581
+ 531
+
+
+
+
+ roiPropPrioritySpinbox
+ valueChanged(int)
+ roiPropPrioritySlider
+ setValue(int)
+
+
+ 581
+ 531
+
+
+ 349
+ 531
+
+
+
+
+ roiPropOuterPrioritySlider
+ valueChanged(int)
+ roiPropOuterPrioritySpinbox
+ setValue(int)
+
+
+ 356
+ 539
+
+
+ 581
+ 538
+
+
+
+
+ roiPropOuterPrioritySpinbox
+ valueChanged(int)
+ roiPropOuterPrioritySlider
+ setValue(int)
+
+
+ 581
+ 538
+
+
+ 356
+ 539
+
+
+
+
diff --git a/UI/frontend-plugins/frontend-tools/roi-editor.cpp b/UI/frontend-plugins/frontend-tools/roi-editor.cpp
index 3d08abfe9e3297..63a3cb0b14eb8a 100644
--- a/UI/frontend-plugins/frontend-tools/roi-editor.cpp
+++ b/UI/frontend-plugins/frontend-tools/roi-editor.cpp
@@ -54,10 +54,26 @@ RoiEditor::RoiEditor(QWidget *parent)
&RoiEditor::PropertiesChanges);
connect(ui->roiPropSizeY, &QSpinBox::valueChanged, this,
&RoiEditor::PropertiesChanges);
- connect(ui->prioritySlider, &QSlider::valueChanged, this,
+ connect(ui->roiPropPrioritySlider, &QSlider::valueChanged, this,
&RoiEditor::PropertiesChanges);
connect(ui->roiPropSceneItem, &QComboBox::currentIndexChanged, this,
&RoiEditor::PropertiesChanges);
+ connect(ui->roiPropEnabled, &QCheckBox::stateChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropOuterPrioritySlider, &QSlider::valueChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropStepsInnerSb, &QSpinBox::valueChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropStepsOuterSb, &QSpinBox::valueChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropRadiusInnerSb, &QSpinBox::valueChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropRadiusOuterSb, &QSpinBox::valueChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropRadiusOuterAspect, &QCheckBox::stateChanged, this,
+ &RoiEditor::PropertiesChanges);
+ connect(ui->roiPropRadiusInnerAspect, &QCheckBox::stateChanged, this,
+ &RoiEditor::PropertiesChanges);
}
void RoiEditor::closeEvent(QCloseEvent *)
@@ -82,26 +98,74 @@ void RoiEditor::PropertiesChanges()
if (!currentItem)
return;
- auto sceneVar = ui->sceneSelect->currentData();
-
RoiData data = {};
- data.priority = (float)ui->prioritySlider->value() / 100.0f;
- data.scene_uuid = sceneVar.toString().toStdString();
+ data.priority = (float)ui->roiPropPrioritySlider->value() / 100.0f;
+ data.enabled = ui->roiPropEnabled->isChecked();
if (currentItem->type() == RoiListItem::SceneItem) {
+ auto sceneVar = ui->sceneSelect->currentData();
+ data.scene_uuid = sceneVar.toString().toStdString();
data.scene_item_id =
ui->roiPropSceneItem->currentData().toLongLong();
- } else {
+ } else if (currentItem->type() == RoiListItem::Manual) {
data.posX = ui->roiPropPosX->value();
data.posY = ui->roiPropPosY->value();
data.width = ui->roiPropSizeX->value();
data.height = ui->roiPropSizeY->value();
- blog(LOG_DEBUG, "Data changed: %d %d %d %d", data.posX,
- data.posY, data.width, data.height);
+ } else if (currentItem->type() == RoiListItem::CenterFocus) {
+ data.inner_radius = ui->roiPropRadiusInnerSb->value();
+ data.inner_aspect = ui->roiPropRadiusInnerAspect->isChecked();
+ data.outer_radius = ui->roiPropRadiusOuterSb->value();
+ data.outer_aspect = ui->roiPropRadiusOuterAspect->isChecked();
+ data.inner_steps = ui->roiPropStepsInnerSb->value();
+ data.outer_steps = ui->roiPropStepsOuterSb->value();
+ data.outer_priority =
+ (float)ui->roiPropOuterPrioritySlider->value() / 100.0f;
}
currentItem->setData(ROIData, QVariant::fromValue(data));
RebuildPreview(true);
+ UpdateEncoderRois();
+}
+
+void RoiEditor::RefreshSceneItems(bool keep_selection)
+{
+ if (!currentItem)
+ return;
+
+ auto sceneVar = ui->sceneSelect->currentData();
+ const string scene_uuid = sceneVar.toString().toStdString();
+ OBSSourceAutoRelease source =
+ obs_get_source_by_uuid(scene_uuid.c_str());
+
+ if (!source)
+ return;
+
+ QVariant current;
+
+ if (keep_selection) {
+ ui->roiPropSceneItem->blockSignals(true);
+ current = ui->roiPropSceneItem->currentData();
+ }
+
+ ui->roiPropSceneItem->clear();
+ obs_scene_enum_items(
+ obs_scene_from_source(source),
+ [](obs_scene_t *, obs_sceneitem_t *item, void *param) -> bool {
+ auto cb = static_cast(param);
+ cb->addItem(obs_source_get_name(
+ obs_sceneitem_get_source(item)),
+ obs_sceneitem_get_id(item));
+ return true;
+ },
+ ui->roiPropSceneItem);
+
+ if (keep_selection) {
+ int idx = ui->roiPropSceneItem->findData(current);
+ if (idx != -1)
+ ui->roiPropSceneItem->setCurrentIndex(idx);
+ ui->roiPropSceneItem->blockSignals(false);
+ }
}
void RoiEditor::ItemSelected(QListWidgetItem *item, QListWidgetItem *)
@@ -114,11 +178,6 @@ void RoiEditor::ItemSelected(QListWidgetItem *item, QListWidgetItem *)
ui->roiPropertiesGroupBox->setVisible(true);
- auto sceneVar = ui->sceneSelect->currentData();
- const string scene_uuid = sceneVar.toString().toStdString();
- OBSSourceAutoRelease source =
- obs_get_source_by_uuid(scene_uuid.c_str());
-
bool manual = false;
bool sceneitem = false;
bool center = false;
@@ -126,56 +185,63 @@ void RoiEditor::ItemSelected(QListWidgetItem *item, QListWidgetItem *)
QVariant var = item->data(ROIData);
RoiData data = var.value();
+ ui->roiPropEnabled->setChecked(data.enabled);
+ ui->roiPropPrioritySlider->setValue((int)(100 * data.priority));
+
+ // Type specific properties
if (item->type() == RoiListItem::SceneItem) {
sceneitem = true;
QSignalBlocker sb(ui->roiPropSceneItem);
- ui->roiPropSceneItem->clear();
-
- if (source) {
- obs_scene_enum_items(
- obs_scene_from_source(source),
- [](obs_scene_t *, obs_sceneitem_t *item,
- void *param) -> bool {
- auto cb =
- static_cast(param);
- cb->addItem(
- obs_source_get_name(
- obs_sceneitem_get_source(
- item)),
- obs_sceneitem_get_id(item));
- return true;
- },
- ui->roiPropSceneItem);
- }
+
+ RefreshSceneItems(false);
int idx = ui->roiPropSceneItem->findData(data.scene_item_id);
if (idx != -1)
ui->roiPropSceneItem->setCurrentIndex(idx);
- ui->prioritySlider->setValue((int)(100 * data.priority));
} else if (item->type() == RoiListItem::Manual) {
manual = true;
ui->roiPropPosX->setValue(data.posX);
ui->roiPropPosY->setValue(data.posY);
ui->roiPropSizeX->setValue(data.width);
ui->roiPropSizeY->setValue(data.height);
- ui->prioritySlider->setValue((int)(100 * data.priority));
+ } else if (item->type() == RoiListItem::CenterFocus) {
+ center = true;
+
+ ui->roiPropOuterPrioritySlider->setValue(
+ (int)(100 * data.outer_priority));
+ ui->roiPropRadiusInnerSb->setValue(data.inner_radius);
+ ui->roiPropRadiusInnerAspect->setChecked(data.inner_aspect);
+ ui->roiPropRadiusOuterSb->setValue(data.outer_radius);
+ ui->roiPropRadiusOuterAspect->setChecked(data.outer_aspect);
+ ui->roiPropStepsInnerSb->setValue(data.inner_steps);
+ ui->roiPropStepsOuterSb->setValue(data.outer_steps);
}
ui->roiPropPosX->setVisible(manual);
ui->roiPropPosY->setVisible(manual);
ui->roiPropPosLabel->setVisible(manual);
-
ui->roiPropSizeLabel->setVisible(manual);
ui->roiPropSizeX->setVisible(manual);
ui->roiPropSizeY->setVisible(manual);
ui->roiPropSceneItem->setVisible(sceneitem);
ui->roiPropSceneItemLabel->setVisible(sceneitem);
- ui->priorityLabel->setVisible(!center);
- ui->prioritySlider->setVisible(!center);
- // ToDo CenterFocus
+ ui->roiPropOuterPriorityLabel->setVisible(center);
+ ui->roiPropOuterPrioritySlider->setVisible(center);
+ ui->roiPropOuterPrioritySpinbox->setVisible(center);
+ ui->roiPropRadiusInnerLabel->setVisible(center);
+ ui->roiPropRadiusInnerSb->setVisible(center);
+ ui->roiPropRadiusInnerAspect->setVisible(center);
+ ui->roiPropRadiusOuterAspect->setVisible(center);
+ ui->roiPropRadiusOuterLabel->setVisible(center);
+ ui->roiPropRadiusOuterSb->setVisible(center);
+ ui->roiPropStepsLabel->setVisible(center);
+ ui->roiPropStepsInnerLabel->setVisible(center);
+ ui->roiPropStepsOuterLabel->setVisible(center);
+ ui->roiPropStepsInnerSb->setVisible(center);
+ ui->roiPropStepsOuterSb->setVisible(center);
}
void RoiEditor::RefreshSceneList()
@@ -186,6 +252,10 @@ void RoiEditor::RefreshSceneList()
obs_enum_scenes(
[](void *param, obs_source_t *src) -> bool {
auto cb = static_cast(param);
+ // screw groups
+ if (obs_source_is_group(src))
+ return true;
+
cb->addItem(obs_source_get_name(src),
obs_source_get_uuid(src));
return true;
@@ -199,6 +269,59 @@ void RoiEditor::RefreshSceneList()
}
}
+/// Builds the list of actual regions of interest that will be passed to OBS
+void RoiEditor::RegionItemsToData()
+{
+ auto var = ui->sceneSelect->currentData();
+ if (!var.isValid())
+ return;
+
+ const string scene_uuid = var.toString().toStdString();
+ roi_data[scene_uuid].clear();
+
+ int count = ui->roiList->count();
+ for (int idx = 0; idx < count; idx++) {
+ auto item = dynamic_cast(ui->roiList->item(idx));
+ if (!item)
+ continue;
+
+ auto var = item->data(ROIData);
+ auto roi = var.value();
+
+ obs_data_t *data = obs_data_create();
+ obs_data_set_double(data, "priority", roi.priority);
+ obs_data_set_bool(data, "enabled", roi.enabled);
+ obs_data_set_int(data, "type", item->type());
+
+ if (item->type() == RoiListItem::SceneItem) {
+ obs_data_set_int(data, "scene_item_id",
+ roi.scene_item_id);
+ } else if (item->type() == RoiListItem::Manual) {
+ obs_data_set_int(data, "x", roi.posX);
+ obs_data_set_int(data, "y", roi.posY);
+ obs_data_set_int(data, "width", roi.width);
+ obs_data_set_int(data, "height", roi.height);
+ } else if (item->type() == RoiListItem::CenterFocus) {
+ obs_data_set_int(data, "center_radius_inner",
+ roi.inner_radius);
+ obs_data_set_int(data, "center_radius_outer",
+ roi.outer_radius);
+ obs_data_set_bool(data, "center_aspect_inner",
+ roi.inner_aspect);
+ obs_data_set_bool(data, "center_aspect_outer",
+ roi.outer_aspect);
+ obs_data_set_int(data, "center_steps_inner",
+ roi.inner_steps);
+ obs_data_set_int(data, "center_steps_outer",
+ roi.outer_steps);
+ obs_data_set_double(data, "center_priority_outer",
+ roi.outer_priority);
+ }
+
+ roi_data[scene_uuid].emplace_back(data);
+ }
+}
+
static region_of_interest GetItemROI(obs_sceneitem_t *item, float priority)
{
region_of_interest roi;
@@ -232,44 +355,106 @@ static region_of_interest GetItemROI(obs_sceneitem_t *item, float priority)
return roi;
}
-/// Builds the list of actual regions of interest that will be passed to OBS
-void RoiEditor::RegionItemsToData()
+static constexpr int64_t kMinBlockSize = 16; // Use H.264 as a baseline
+
+static void BuildInnerRegions(vector &rois, float priority,
+ int64_t steps, int64_t radius,
+ bool correct_aspect, uint32_t width,
+ uint32_t height)
{
- auto var = ui->sceneSelect->currentData();
- if (!var.isValid())
+ if (!radius || height < radius || width < radius ||
+ radius < kMinBlockSize / 2 || priority == 0.0 || !steps)
return;
+ int64_t interval = radius / steps;
- const string scene_uuid = var.toString().toStdString();
- roi_data[scene_uuid].clear();
+ /* Clamp interval size and step count to the smallest block size */
+ if (interval < kMinBlockSize / 2) {
+ interval = kMinBlockSize / 2;
+ steps = std::max(radius / interval, 1LL);
+ }
- int count = ui->roiList->count();
- for (int idx = 0; idx < count; idx++) {
- auto item = dynamic_cast(ui->roiList->item(idx));
- if (!item)
- continue;
+ double priority_interval = priority / (double)steps;
+ double aspect = 1.0;
+ if (correct_aspect)
+ aspect = (double)width / (double)height;
+
+ uint32_t middle_x = width / 2;
+ uint32_t middle_y = height / 2;
+
+ for (int i = 1; i <= steps; i++) {
+ region_of_interest roi = {
+ (uint32_t)(middle_y - interval * i),
+ (uint32_t)(middle_y + interval * i),
+ (uint32_t)(middle_x - interval * i * aspect),
+ (uint32_t)(middle_x + interval * i * aspect),
+ (float)(priority - priority_interval * (i - 1))};
+ rois.push_back(roi);
+ }
+}
- if (item->type() == RoiListItem::SceneItem ||
- item->type() == RoiListItem::Manual) {
- auto var = item->data(ROIData);
- auto roi = var.value();
-
- obs_data_t *data = obs_data_create();
- obs_data_set_double(data, "priority", roi.priority);
-
- if (item->type() == RoiListItem::SceneItem) {
- obs_data_set_int(data, "scene_item_id",
- roi.scene_item_id);
- } else {
- obs_data_set_int(data, "x", roi.posX);
- obs_data_set_int(data, "y", roi.posY);
- obs_data_set_int(data, "width", roi.width);
- obs_data_set_int(data, "height", roi.height);
- }
+static void BuildOuterRegions(vector &rois, float priority,
+ int64_t steps, int64_t radius,
+ bool correct_aspect, uint32_t width,
+ uint32_t height)
+{
+ if (!radius || height / 2 < radius || width / 2 < radius ||
+ radius < kMinBlockSize || priority == 0.0 || !steps)
+ return;
- roi_data[scene_uuid].emplace_back(data);
- }
- // ToDo RoiListItem::CenterFocus / MultiROIData
+ /* Clamp interval size and step count to the smallest block size */
+ int64_t interval = radius / steps;
+ if (interval < kMinBlockSize) {
+ interval = kMinBlockSize;
+ steps = std::max(radius / interval, 1LL);
+ }
+
+ double priority_interval = priority / (double)steps;
+ double aspect = 1.0;
+ if (correct_aspect)
+ aspect = (double)width / (double)height;
+
+ /* Add neutral baseline */
+ region_of_interest neutral = {
+ (uint32_t)radius, (uint32_t)(height - radius),
+ (uint32_t)((double)radius * aspect),
+ (uint32_t)(width - (double)radius * aspect), 0.0f};
+ rois.push_back(neutral);
+
+ for (int i = 1; steps > 1 && i < steps; i++) {
+ region_of_interest roi = {
+ (uint32_t)(radius - interval * i),
+ (uint32_t)(height - radius + interval * i),
+ (uint32_t)((double)(radius - interval * i) * aspect),
+ (uint32_t)(width -
+ (double)(radius - interval * i) * aspect),
+ (float)(priority_interval * i)};
+ rois.push_back(roi);
}
+
+ /* Ensure last region always goes to frame edges */
+ region_of_interest final = {0, height, 0, width, (float)priority};
+ rois.push_back(final);
+}
+
+static void BuildCenterFocusROI(vector &rois,
+ obs_data_t *data, uint32_t width,
+ uint32_t height)
+{
+ int64_t inner_radius = obs_data_get_int(data, "center_radius_inner");
+ bool aspect_inner = obs_data_get_bool(data, "center_aspect_inner");
+ int64_t outer_radius = obs_data_get_int(data, "center_radius_outer");
+ bool aspect_outer = obs_data_get_bool(data, "center_aspect_outer");
+ int64_t steps_inner = obs_data_get_int(data, "center_steps_inner");
+ int64_t steps_outer = obs_data_get_int(data, "center_steps_outer");
+ double priority_outer =
+ obs_data_get_double(data, "center_priority_outer");
+ double priority = obs_data_get_double(data, "priority");
+
+ /* Inner regions (if any) */
+ BuildInnerRegions(rois, priority, steps_inner, inner_radius,
+ aspect_inner, width, height);
+ BuildOuterRegions(rois, priority_outer, steps_outer, outer_radius,
+ aspect_outer, width, height);
}
void RoiEditor::RegionsFromData(vector &rois,
@@ -279,13 +464,21 @@ void RoiEditor::RegionsFromData(vector &rois,
if (regions.empty())
return;
OBSSourceAutoRelease source = obs_get_source_by_uuid(uuid.c_str());
+ if (!source)
+ return;
for (obs_data_t *data : regions) {
float priority = obs_data_get_double(data, "priority");
- if (obs_data_has_user_value(data, "scene_item_id")) {
+ if (!obs_data_get_bool(data, "enabled"))
+ continue;
+
+ auto type = (RoiListItem::RoiItemType)obs_data_get_int(data,
+ "type");
+
+ if (type == RoiListItem::SceneItem) {
/* Scene Item ROI */
- uint64_t id = obs_data_get_int(data, "scene_item_id");
+ int64_t id = obs_data_get_int(data, "scene_item_id");
OBSSceneItem sceneItem = obs_scene_find_sceneitem_by_id(
obs_scene_from_source(source), id);
@@ -295,7 +488,7 @@ void RoiEditor::RegionsFromData(vector &rois,
rois.push_back(roi);
}
- } else if (obs_data_has_user_value(data, "x")) {
+ } else if (type == RoiListItem::Manual) {
/* Fixed ROI */
uint32_t left = (uint32_t)obs_data_get_int(data, "x");
uint32_t top = (uint32_t)obs_data_get_int(data, "y");
@@ -311,9 +504,11 @@ void RoiEditor::RegionsFromData(vector &rois,
region_of_interest roi{top, bottom, left, right,
priority};
rois.push_back(roi);
- } else if (obs_data_has_user_value(data, "radius")) {
+ } else if (type == RoiListItem::CenterFocus) {
/* Center-focus ROI */
- // ToDo
+ uint32_t cx = obs_source_get_width(source);
+ uint32_t cy = obs_source_get_height(source);
+ BuildCenterFocusROI(rois, data, cx, cy);
}
}
}
@@ -363,11 +558,13 @@ void RoiEditor::RebuildPreview(bool rebuildData)
obs_video_info ovi;
obs_get_video_info(&ovi);
- uint32_t blocks_w = (ovi.base_width + (blockSize - 1)) / blockSize;
- uint32_t blocks_h = (ovi.base_height + (blockSize - 1)) / blockSize;
+ QSize dimensions((ovi.base_width + (blockSize - 1)) / blockSize,
+ (ovi.base_height + (blockSize - 1)) / blockSize);
+
+ // Resize pixmap if necessary
+ if (previewPixmap.size() != dimensions)
+ previewPixmap = QPixmap(dimensions);
- // Draw new pixmap
- previewPixmap = QPixmap(blocks_w, blocks_h);
previewPixmap.fill(Qt::black);
QPainter paint(&previewPixmap);
paint.setPen(Qt::PenStyle::NoPen);
@@ -380,7 +577,6 @@ void RoiEditor::RebuildPreview(bool rebuildData)
previewScene->clear();
previewScene->addPixmap(previewPixmap);
previewScene->setSceneRect(previewPixmap.rect());
-
ui->preview->setScene(previewScene);
ui->preview->fitInView(ui->preview->scene()->sceneRect(),
Qt::KeepAspectRatio);
@@ -389,15 +585,21 @@ void RoiEditor::RebuildPreview(bool rebuildData)
void RoiEditor::SceneItemTransform(void *param, calldata_t *)
{
RoiEditor *window = reinterpret_cast(param);
- window->RebuildPreview();
- window->UpdateEncoderRois();
+ QMetaObject::invokeMethod(window, "RebuildPreview");
+ QMetaObject::invokeMethod(window, "UpdateEncoderRois");
}
void RoiEditor::SceneItemVisibility(void *param, calldata_t *, bool)
{
RoiEditor *window = reinterpret_cast(param);
- window->RebuildPreview();
- window->UpdateEncoderRois();
+ QMetaObject::invokeMethod(window, "RebuildPreview");
+ QMetaObject::invokeMethod(window, "UpdateEncoderRois");
+}
+
+void RoiEditor::ItemRemovedOrAdded(void *param, calldata_t *)
+{
+ RoiEditor *window = reinterpret_cast(param);
+ QMetaObject::invokeMethod(window, "RefreshSceneItems");
}
void RoiEditor::ConnectSceneSignals()
@@ -408,6 +610,10 @@ void RoiEditor::ConnectSceneSignals()
this);
visibilitySignal.Connect(signal, "item_visible", SceneItemTransform,
this);
+ itemAddedSignal.Connect(signal, "item_add", ItemRemovedOrAdded, this);
+ itemRemovedSignal.Connect(signal, "item_remove", ItemRemovedOrAdded,
+ this);
+ sceneRefreshSignal.Connect(signal, "refresh", ItemRemovedOrAdded, this);
}
void RoiEditor::RefreshRoiList()
@@ -420,9 +626,8 @@ void RoiEditor::RefreshRoiList()
const string scene_uuid = var.toString().toStdString();
for (obs_data_t *roi : roi_data[scene_uuid]) {
- int type = obs_data_has_user_value(roi, "scene_item_id")
- ? RoiListItem::SceneItem
- : RoiListItem::Manual;
+ auto type =
+ (RoiListItem::RoiItemType)obs_data_get_int(roi, "type");
RoiData data = {
scene_uuid,
@@ -431,6 +636,15 @@ void RoiEditor::RefreshRoiList()
(uint32_t)obs_data_get_int(roi, "y"),
(uint32_t)obs_data_get_int(roi, "width"),
(uint32_t)obs_data_get_int(roi, "height"),
+ obs_data_get_int(roi, "center_radius_inner"),
+ obs_data_get_int(roi, "center_steps_inner"),
+ obs_data_get_bool(roi, "center_aspect_inner"),
+ obs_data_get_int(roi, "center_radius_outer"),
+ obs_data_get_int(roi, "center_steps_outer"),
+ (float)obs_data_get_double(roi,
+ "center_priority_outer"),
+ obs_data_get_bool(roi, "center_aspect_outer"),
+ obs_data_get_bool(roi, "enabled"),
(float)obs_data_get_double(roi, "priority"),
};
@@ -447,50 +661,12 @@ void RoiEditor::SceneSelectionChanged()
// ToDo refresh list of actual items
}
-void RoiEditor::LoadRoisFromOBSData(obs_data_t *obj)
-{
- ui->enableRoi->setChecked(obs_data_get_bool(obj, "enabled"));
- OBSDataAutoRelease scenes = obs_data_get_obj(obj, "scenes");
- obs_data_item *item = obs_data_first(scenes);
-
- roi_data.clear();
-
- while (item) {
- const char *uuid = obs_data_item_get_name(item);
-
- OBSDataArrayAutoRelease arr = obs_data_item_get_array(item);
- size_t count = obs_data_array_count(arr);
-
- for (size_t idx = 0; idx < count; idx++) {
- roi_data[uuid].emplace_back(
- obs_data_array_item(arr, idx));
- }
-
- obs_data_item_next(&item);
- }
-}
-
-void RoiEditor::SaveRoisToOBSData(obs_data_t *obj)
-{
- OBSDataAutoRelease scenes = obs_data_create();
-
- for (const auto &item : roi_data) {
- obs_data_array_t *scene = obs_data_array_create();
-
- for (const auto &roi : item.second)
- obs_data_array_push_back(scene, roi);
-
- obs_data_set_array(scenes, item.first.c_str(), scene);
- obs_data_array_release(scene);
- }
-
- obs_data_set_obj(obj, "scenes", scenes);
- obs_data_set_bool(obj, "enabled", ui->enableRoi->isChecked());
-}
-
void RoiEditor::UpdateEncoderRois()
{
OBSSourceAutoRelease scene = obs_frontend_get_current_scene();
+ if (!scene)
+ return;
+
const string uuid = obs_source_get_uuid(scene);
std::vector encoders;
@@ -515,7 +691,6 @@ void RoiEditor::UpdateEncoderRois()
return;
// Clear any ROIs that might exist
- blog(LOG_DEBUG, "Clearing ROIs...");
for (obs_encoder_t *enc : encoders)
obs_encoder_clear_roi(enc);
@@ -543,31 +718,51 @@ void RoiEditor::UpdateEncoderRois()
void RoiEditor::on_actionAddRoi_triggered()
{
- auto popup = QMenu(obs_module_text("AddROI"), this);
+ auto popup = QMenu(obs_module_text("ROI.AddMenu"), this);
QAction *addSceneItemRoi =
- new QAction(obs_module_text("ROI.AddSceneItem"), this);
+ new QAction(obs_module_text("ROI.AddMenu.SceneItem"), this);
connect(addSceneItemRoi, &QAction::triggered, [this] {
auto item =
new RoiListItem(ui->roiList, RoiListItem::SceneItem);
ui->roiList->addItem(item);
RoiData roi = {};
+ roi.enabled = true;
item->setData(ROIData, QVariant::fromValue(roi));
RegionItemsToData();
+ RebuildPreview(true);
});
popup.insertAction(nullptr, addSceneItemRoi);
QAction *addManualRoi =
- new QAction(obs_module_text("ROI.AddManual"), this);
+ new QAction(obs_module_text("ROI.AddMenu.Manual"), this);
connect(addManualRoi, &QAction::triggered, [this] {
auto item = new RoiListItem(ui->roiList, RoiListItem::Manual);
ui->roiList->addItem(item);
- RoiData roi = {"", 0, 0, 0, 100, 100, 0.0f};
+ RoiData roi = {};
+ roi.height = 100;
+ roi.width = 100;
+ roi.enabled = true;
item->setData(ROIData, QVariant::fromValue(roi));
RegionItemsToData();
+ RebuildPreview(true);
});
popup.insertAction(addSceneItemRoi, addManualRoi);
+ QAction *addCenterRoi =
+ new QAction(obs_module_text("ROI.AddMenu.CenterFocus"), this);
+ connect(addCenterRoi, &QAction::triggered, [this] {
+ auto item =
+ new RoiListItem(ui->roiList, RoiListItem::CenterFocus);
+ ui->roiList->addItem(item);
+ RoiData roi = {};
+ roi.enabled = true;
+ item->setData(ROIData, QVariant::fromValue(roi));
+ RegionItemsToData();
+ RebuildPreview(true);
+ });
+ popup.insertAction(addManualRoi, addCenterRoi);
+
popup.exec(QCursor::pos());
}
@@ -616,6 +811,47 @@ void RoiEditor::resizeEvent(QResizeEvent *event)
QDialog::resizeEvent(event);
}
+void RoiEditor::LoadRoisFromOBSData(obs_data_t *obj)
+{
+ ui->enableRoi->setChecked(obs_data_get_bool(obj, "enabled"));
+ OBSDataAutoRelease scenes = obs_data_get_obj(obj, "scenes");
+ obs_data_item *item = obs_data_first(scenes);
+
+ roi_data.clear();
+
+ while (item) {
+ const char *uuid = obs_data_item_get_name(item);
+
+ OBSDataArrayAutoRelease arr = obs_data_item_get_array(item);
+ size_t count = obs_data_array_count(arr);
+
+ for (size_t idx = 0; idx < count; idx++) {
+ roi_data[uuid].emplace_back(
+ obs_data_array_item(arr, idx));
+ }
+
+ obs_data_item_next(&item);
+ }
+}
+
+void RoiEditor::SaveRoisToOBSData(obs_data_t *obj)
+{
+ OBSDataAutoRelease scenes = obs_data_create();
+
+ for (const auto &item : roi_data) {
+ obs_data_array_t *scene = obs_data_array_create();
+
+ for (const auto &roi : item.second)
+ obs_data_array_push_back(scene, roi);
+
+ obs_data_set_array(scenes, item.first.c_str(), scene);
+ obs_data_array_release(scene);
+ }
+
+ obs_data_set_obj(obj, "scenes", scenes);
+ obs_data_set_bool(obj, "enabled", ui->enableRoi->isChecked());
+}
+
/*
* RoiListItem
*/
@@ -632,14 +868,23 @@ void RoiListItem::setData(int role, const QVariant &value)
if (role == ROIData) {
roi = value.value();
+ QString desc;
+
+ // This needs to be prettier at some point
+ if (!roi.enabled) {
+ desc += "[";
+ desc += obs_module_text("ROI.Item.DisabledPrefix");
+ desc += "] ";
+ }
+
if (type() == Manual) {
- setText(QString(obs_module_text("ROI.ManualRegion"))
+ desc += QString(obs_module_text(
+ "ROI.Item.ManualRegion"))
.arg(roi.width)
.arg(roi.height)
.arg(roi.posX)
- .arg(roi.posY));
- } else {
- // ToDo get scene item name?
+ .arg(roi.posY);
+ } else if (type() == SceneItem) {
OBSSourceAutoRelease source =
obs_get_source_by_uuid(roi.scene_uuid.c_str());
obs_scene_item *sceneItem =
@@ -649,11 +894,14 @@ void RoiListItem::setData(int role, const QVariant &value)
const char *name = obs_source_get_name(
obs_sceneitem_get_source(sceneItem));
- setText(QString(obs_module_text("ROI.SceneItem"))
+ desc += QString(obs_module_text("ROI.Item.SceneItem"))
.arg(name)
- .arg(roi.scene_item_id));
+ .arg(roi.scene_item_id);
+ } else {
+ desc += obs_module_text("ROI.Item.CenterFocus");
}
+ setText(desc);
return;
}
@@ -696,10 +944,11 @@ static void OBSEvent(obs_frontend_event event, void *)
extern "C" void FreeRoiEditor() {}
+// ToDo websocket vendor request
extern "C" void InitRoiEditor()
{
QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(
- obs_module_text("ROI.Editor"));
+ obs_module_text("ROIEditor"));
obs_frontend_push_ui_translation(obs_module_get_string);
@@ -707,7 +956,7 @@ extern "C" void InitRoiEditor()
roi_edit = new RoiEditor(window);
- auto cb = []() {
+ auto cb = [] {
roi_edit->ShowHideDialog();
};
diff --git a/UI/frontend-plugins/frontend-tools/roi-editor.hpp b/UI/frontend-plugins/frontend-tools/roi-editor.hpp
index 8209d020bafdf6..d9ca9310366f2f 100644
--- a/UI/frontend-plugins/frontend-tools/roi-editor.hpp
+++ b/UI/frontend-plugins/frontend-tools/roi-editor.hpp
@@ -1,5 +1,4 @@
#pragma once
-#pragma once
#include "ui_roi-editor.h"
@@ -9,9 +8,21 @@ class RoiListItem;
class QCloseEvent;
struct RoiData {
+ /* Scene item type*/
std::string scene_uuid;
int64_t scene_item_id;
+ /* Manual type */
uint32_t posX, posY, width, height;
+ /* Center focus type */
+ int64_t inner_radius;
+ int64_t inner_steps;
+ bool inner_aspect;
+ int64_t outer_radius;
+ int64_t outer_steps;
+ float outer_priority;
+ bool outer_aspect;
+ /* Shared attributes */
+ bool enabled;
float priority;
};
Q_DECLARE_METATYPE(RoiData)
@@ -29,26 +40,27 @@ class RoiEditor : public QDialog {
public slots:
void ShowHideDialog();
- void ItemSelected(QListWidgetItem *item, QListWidgetItem *);
void LoadRoisFromOBSData(obs_data_t *obj);
void SaveRoisToOBSData(obs_data_t *obj);
- void UpdateEncoderRois();
void ConnectSceneSignals();
+ void UpdateEncoderRois();
- // ToDo event listeners for output start to apply ROI
- // ToDo events for scene item and active scene changes (movement/visibility)
private slots:
void on_actionAddRoi_triggered();
void on_actionRemoveRoi_triggered();
void on_actionRoiUp_triggered();
void on_actionRoiDown_triggered();
- void resizeEvent(QResizeEvent *event);
+ void resizeEvent(QResizeEvent *event) override;
+
+ void ItemSelected(QListWidgetItem *item, QListWidgetItem *);
+
+ void RefreshSceneItems(bool keep_selection = true);
+ void RebuildPreview(bool rebuildData = false);
private:
void RefreshSceneList();
void RefreshRoiList();
- void RebuildPreview(bool rebuilData = false);
void SceneSelectionChanged();
void RegionItemsToData();
void RegionsFromData(std::vector &rois,
@@ -59,6 +71,7 @@ private slots:
static void SceneItemTransform(void *param, calldata_t *data);
static void SceneItemVisibility(void *param, calldata_t *data,
bool visible);
+ static void ItemRemovedOrAdded(void *param, calldata_t *data);
// key is scene UUID
std::unordered_map>
@@ -66,16 +79,16 @@ private slots:
OBSSignal transformSignal;
OBSSignal visibilitySignal;
+ OBSSignal itemAddedSignal;
+ OBSSignal itemRemovedSignal;
+ OBSSignal sceneRefreshSignal;
QGraphicsScene *previewScene;
QPixmap previewPixmap;
RoiListItem *currentItem = nullptr;
};
-enum ROIDataRoles {
- ROIData = Qt::UserRole,
- CenterFocusData,
-};
+enum ROIDataRoles { ROIData = Qt::UserRole };
class RoiListItem : public QListWidgetItem {