Added "save/load session" menu actions.
git-svn-id: http://find-object.googlecode.com/svn/trunk/find_object@418 620bd6b2-0a58-f614-fd9a-1bd335dccda9
This commit is contained in:
parent
84166e6ddc
commit
02e31c2e69
@ -65,6 +65,9 @@ public:
|
||||
FindObject(QObject * parent = 0);
|
||||
virtual ~FindObject();
|
||||
|
||||
bool loadSession(const QString & path);
|
||||
bool saveSession(const QString & path) const;
|
||||
|
||||
int loadObjects(const QString & dirPath); // call updateObjects()
|
||||
const ObjSignature * addObject(const QString & filePath);
|
||||
const ObjSignature * addObject(const cv::Mat & image, int id=0, const QString & filename = QString());
|
||||
|
||||
@ -81,6 +81,8 @@ public Q_SLOTS:
|
||||
void update(const cv::Mat & image);
|
||||
|
||||
private Q_SLOTS:
|
||||
void loadSession();
|
||||
void saveSession();
|
||||
void loadSettings();
|
||||
void saveSettings();
|
||||
void loadObjects();
|
||||
@ -110,7 +112,7 @@ Q_SIGNALS:
|
||||
|
||||
private:
|
||||
bool loadSettings(const QString & path);
|
||||
bool saveSettings(const QString & path);
|
||||
bool saveSettings(const QString & path) const;
|
||||
int loadObjects(const QString & dirPath);
|
||||
int saveObjects(const QString & dirPath);
|
||||
void setupTCPServer();
|
||||
|
||||
@ -358,7 +358,7 @@ void AddObjectDialog::setState(int state)
|
||||
objSignature_ = 0;
|
||||
}
|
||||
objSignature_ = new ObjSignature(0, imgRoi.clone(), "");
|
||||
objSignature_->setData(keypoints, descriptors, Settings::currentDetectorType(), Settings::currentDescriptorType());
|
||||
objSignature_->setData(keypoints, descriptors);
|
||||
objWidget_ = new ObjWidget(0, keypoints, cvtCvMat2QImage(imgRoi.clone()));
|
||||
|
||||
this->accept();
|
||||
|
||||
@ -59,6 +59,85 @@ FindObject::~FindObject() {
|
||||
objectsDescriptors_.clear();
|
||||
}
|
||||
|
||||
bool FindObject::loadSession(const QString & path)
|
||||
{
|
||||
if(QFile::exists(path) && !path.isEmpty() && QFileInfo(path).suffix().compare("bin") == 0)
|
||||
{
|
||||
QFile file(path);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
QDataStream in(&file);
|
||||
|
||||
ParametersMap parameters;
|
||||
|
||||
// load parameters
|
||||
in >> parameters;
|
||||
for(QMap<QString, QVariant>::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter)
|
||||
{
|
||||
Settings::setParameter(iter.key(), iter.value());
|
||||
}
|
||||
|
||||
// save vocabulary
|
||||
vocabulary_->load(in);
|
||||
|
||||
// load objects
|
||||
while(!in.atEnd())
|
||||
{
|
||||
ObjSignature * obj = new ObjSignature();
|
||||
obj->load(in);
|
||||
if(obj->id() >= 0)
|
||||
{
|
||||
objects_.insert(obj->id(), obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
UERROR("Failed to load and object!");
|
||||
delete obj;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
|
||||
if(!Settings::getGeneral_invertedSearch())
|
||||
{
|
||||
// this will fill objectsDescriptors_ matrix
|
||||
updateVocabulary();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
UERROR("Invalid session file (should be *.bin): \"%s\"", path.toStdString().c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FindObject::saveSession(const QString & path) const
|
||||
{
|
||||
if(!path.isEmpty() && QFileInfo(path).suffix().compare("bin") == 0)
|
||||
{
|
||||
QFile file(path);
|
||||
file.open(QIODevice::WriteOnly);
|
||||
QDataStream out(&file);
|
||||
|
||||
// save parameters
|
||||
out << Settings::getParameters();
|
||||
|
||||
// save vocabulary
|
||||
vocabulary_->save(out);
|
||||
|
||||
// save objects
|
||||
for(QMultiMap<int, ObjSignature*>::const_iterator iter=objects_.constBegin(); iter!=objects_.constEnd(); ++iter)
|
||||
{
|
||||
iter.value()->save(out);
|
||||
}
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
UERROR("Path \"%s\" not valid (should be *.bin)", path.toStdString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
int FindObject::loadObjects(const QString & dirPath)
|
||||
{
|
||||
int loadedObjects = 0;
|
||||
@ -143,7 +222,6 @@ bool FindObject::addObject(ObjSignature * obj)
|
||||
Settings::setGeneral_nextObjID(obj->id()+1);
|
||||
|
||||
objects_.insert(obj->id(), obj);
|
||||
clearVocabulary();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -512,11 +590,7 @@ void FindObject::updateObjects()
|
||||
|
||||
int id = threads[j]->objectId();
|
||||
|
||||
objects_.value(id)->setData(
|
||||
threads[j]->keypoints(),
|
||||
threads[j]->descriptors(),
|
||||
Settings::currentDetectorType(),
|
||||
Settings::currentDescriptorType());
|
||||
objects_.value(id)->setData(threads[j]->keypoints(), threads[j]->descriptors());
|
||||
}
|
||||
}
|
||||
UINFO("Features extraction from %d objects... done! (%d ms)", objects_.size(), time.elapsed());
|
||||
@ -915,13 +989,22 @@ bool FindObject::detect(const cv::Mat & image, find_object::DetectionInfo & info
|
||||
bool consistentNNData = (vocabulary_->size()!=0 && vocabulary_->wordToObjects().begin().value()!=-1 && Settings::getGeneral_invertedSearch()) ||
|
||||
((vocabulary_->size()==0 || vocabulary_->wordToObjects().begin().value()==-1) && !Settings::getGeneral_invertedSearch());
|
||||
|
||||
bool descriptorsValid = !Settings::getGeneral_invertedSearch() &&
|
||||
!objectsDescriptors_.empty() &&
|
||||
objectsDescriptors_.begin().value().cols == info.sceneDescriptors_.cols &&
|
||||
objectsDescriptors_.begin().value().type() == info.sceneDescriptors_.type();
|
||||
|
||||
bool vocabularyValid = Settings::getGeneral_invertedSearch() &&
|
||||
vocabulary_->size() &&
|
||||
!vocabulary_->indexedDescriptors().empty() &&
|
||||
vocabulary_->indexedDescriptors().cols == info.sceneDescriptors_.cols &&
|
||||
vocabulary_->indexedDescriptors().type() == info.sceneDescriptors_.type();
|
||||
|
||||
// COMPARE
|
||||
UDEBUG("COMPARE");
|
||||
if(!objectsDescriptors_.empty() &&
|
||||
if((descriptorsValid || vocabularyValid) &&
|
||||
info.sceneKeypoints_.size() &&
|
||||
consistentNNData &&
|
||||
objectsDescriptors_.begin().value().cols == info.sceneDescriptors_.cols &&
|
||||
objectsDescriptors_.begin().value().type() == info.sceneDescriptors_.type()) // binary descriptor issue, if the dataTree is not yet updated with modified settings
|
||||
consistentNNData)
|
||||
{
|
||||
success = true;
|
||||
QTime time;
|
||||
@ -1243,7 +1326,7 @@ bool FindObject::detect(const cv::Mat & image, find_object::DetectionInfo & info
|
||||
info.timeStamps_.insert(DetectionInfo::kTimeHomography, time.restart());
|
||||
}
|
||||
}
|
||||
else if(!objectsDescriptors_.empty() && info.sceneKeypoints_.size())
|
||||
else if((descriptorsValid || vocabularyValid) && info.sceneKeypoints_.size())
|
||||
{
|
||||
UWARN("Cannot search, objects must be updated");
|
||||
}
|
||||
|
||||
@ -181,6 +181,7 @@ MainWindow::MainWindow(FindObject * findObject, Camera * camera, QWidget * paren
|
||||
ui_->actionStop_camera->setEnabled(false);
|
||||
ui_->actionPause_camera->setEnabled(false);
|
||||
ui_->actionSave_objects->setEnabled(false);
|
||||
ui_->actionSave_session->setEnabled(false);
|
||||
|
||||
// Actions
|
||||
connect(ui_->actionAdd_object_from_scene, SIGNAL(triggered()), this, SLOT(addObjectFromScene()));
|
||||
@ -200,6 +201,8 @@ MainWindow::MainWindow(FindObject * findObject, Camera * camera, QWidget * paren
|
||||
connect(ui_->actionRemove_all_objects, SIGNAL(triggered()), this, SLOT(removeAllObjects()));
|
||||
connect(ui_->actionSave_settings, SIGNAL(triggered()), this, SLOT(saveSettings()));
|
||||
connect(ui_->actionLoad_settings, SIGNAL(triggered()), this, SLOT(loadSettings()));
|
||||
connect(ui_->actionSave_session, SIGNAL(triggered()), this, SLOT(saveSession()));
|
||||
connect(ui_->actionLoad_session, SIGNAL(triggered()), this, SLOT(loadSession()));
|
||||
connect(ui_->actionShow_objects_features, SIGNAL(triggered()), this, SLOT(showObjectsFeatures()));
|
||||
connect(ui_->actionHide_objects_features, SIGNAL(triggered()), this, SLOT(hideObjectsFeatures()));
|
||||
|
||||
@ -315,6 +318,84 @@ void MainWindow::setSourceImageText(const QString & text)
|
||||
ui_->imageView_source->setTextLabel(text);
|
||||
}
|
||||
|
||||
void MainWindow::loadSession()
|
||||
{
|
||||
if(objWidgets_.size())
|
||||
{
|
||||
QMessageBox::StandardButton b = QMessageBox::question(this, tr("Loading session..."),
|
||||
tr("There are some objects (%1) already loaded, they will be "
|
||||
"deleted when loading the session. Do you want to continue?").arg(objWidgets_.size()),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton);
|
||||
if(b != QMessageBox::Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QString path = QFileDialog::getOpenFileName(this, tr("Load session..."), Settings::workingDirectory(), "*.bin");
|
||||
if(!path.isEmpty())
|
||||
{
|
||||
qDeleteAll(objWidgets_);
|
||||
objWidgets_.clear();
|
||||
ui_->actionSave_objects->setEnabled(false);
|
||||
findObject_->removeAllObjects();
|
||||
|
||||
if(findObject_->loadSession(path))
|
||||
{
|
||||
//update parameters tool box
|
||||
const ParametersMap & parameters = Settings::getParameters();
|
||||
for(ParametersMap::const_iterator iter = parameters.begin(); iter!= parameters.constEnd(); ++iter)
|
||||
{
|
||||
ui_->toolBox->updateParameter(iter.key());
|
||||
}
|
||||
|
||||
//update object widgets
|
||||
for(QMap<int, ObjSignature *>::const_iterator iter=findObject_->objects().constBegin(); iter!=findObject_->objects().constEnd();++iter)
|
||||
{
|
||||
if(iter.value())
|
||||
{
|
||||
ObjWidget * obj = new ObjWidget(iter.key(), iter.value()->keypoints(), cvtCvMat2QImage(iter.value()->image()));
|
||||
objWidgets_.insert(obj->id(), obj);
|
||||
ui_->actionSave_objects->setEnabled(true);
|
||||
ui_->actionSave_session->setEnabled(true);
|
||||
this->showObject(obj);
|
||||
|
||||
//update object labels
|
||||
QLabel * title = qFindChild<QLabel*>(this, QString("%1title").arg(iter.value()->id()));
|
||||
title->setText(QString("%1 (%2)").arg(iter.value()->id()).arg(QString::number(iter.value()->keypoints().size())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QMessageBox::information(this, tr("Session loaded!"), tr("Session \"%1\" successfully loaded (%2 objects)!").arg(path).arg(objWidgets_.size()));
|
||||
|
||||
if(!camera_->isRunning() && !sceneImage_.empty())
|
||||
{
|
||||
this->update(sceneImage_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void MainWindow::saveSession()
|
||||
{
|
||||
if(objWidgets_.size())
|
||||
{
|
||||
QString path = QFileDialog::getSaveFileName(this, tr("Save session..."), Settings::workingDirectory(), "*.bin");
|
||||
if(!path.isEmpty())
|
||||
{
|
||||
if(QFileInfo(path).suffix().compare("bin") != 0)
|
||||
{
|
||||
path.append(".bin");
|
||||
}
|
||||
|
||||
if(findObject_->saveSession(path))
|
||||
{
|
||||
QMessageBox::information(this, tr("Session saved!"), tr("Session \"%1\" successfully saved (%2 objects)!").arg(path).arg(objWidgets_.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadSettings()
|
||||
{
|
||||
QString path = QFileDialog::getOpenFileName(this, tr("Load settings..."), Settings::workingDirectory(), "*.ini");
|
||||
@ -360,11 +441,11 @@ bool MainWindow::loadSettings(const QString & path)
|
||||
|
||||
return true;
|
||||
}
|
||||
UINFO("Path \"%s\" not valid (should be *.ini)", path.toStdString().c_str());
|
||||
UERROR("Path \"%s\" not valid (should be *.ini)", path.toStdString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MainWindow::saveSettings(const QString & path)
|
||||
bool MainWindow::saveSettings(const QString & path) const
|
||||
{
|
||||
if(!path.isEmpty() && QFileInfo(path).suffix().compare("ini") == 0)
|
||||
{
|
||||
@ -372,7 +453,7 @@ bool MainWindow::saveSettings(const QString & path)
|
||||
Settings::saveWindowSettings(this->saveGeometry(), this->saveState(), path);
|
||||
return true;
|
||||
}
|
||||
UINFO("Path \"%s\" not valid (should be *.ini)", path.toStdString().c_str());
|
||||
UERROR("Path \"%s\" not valid (should be *.ini)", path.toStdString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -405,7 +486,7 @@ int MainWindow::saveObjects(const QString & dirPath)
|
||||
QDir dir(dirPath);
|
||||
if(dir.exists())
|
||||
{
|
||||
for(QMap<int, ObjWidget*>::iterator iter=objWidgets_.begin(); iter!=objWidgets_.end(); ++iter)
|
||||
for(QMap<int, ObjWidget*>::const_iterator iter=objWidgets_.constBegin(); iter!=objWidgets_.constEnd(); ++iter)
|
||||
{
|
||||
if(iter.value()->pixmap().save(QString("%1/%2.png").arg(dirPath).arg(iter.key())))
|
||||
{
|
||||
@ -464,6 +545,7 @@ void MainWindow::removeObject(find_object::ObjWidget * object)
|
||||
if(objWidgets_.size() == 0)
|
||||
{
|
||||
ui_->actionSave_objects->setEnabled(false);
|
||||
ui_->actionSave_session->setEnabled(false);
|
||||
}
|
||||
findObject_->removeObject(object->id());
|
||||
object->deleteLater();
|
||||
@ -570,6 +652,7 @@ void MainWindow::addObjectFromScene()
|
||||
obj->setId(signature->id());
|
||||
objWidgets_.insert(obj->id(), obj);
|
||||
ui_->actionSave_objects->setEnabled(true);
|
||||
ui_->actionSave_session->setEnabled(true);
|
||||
showObject(obj);
|
||||
updateVocabulary();
|
||||
objectsModified_ = true;
|
||||
@ -616,6 +699,7 @@ bool MainWindow::addObjectFromFile(const QString & filePath)
|
||||
ObjWidget * obj = new ObjWidget(s->id(), std::vector<cv::KeyPoint>(), cvtCvMat2QImage(s->image()));
|
||||
objWidgets_.insert(obj->id(), obj);
|
||||
ui_->actionSave_objects->setEnabled(true);
|
||||
ui_->actionSave_session->setEnabled(true);
|
||||
this->showObject(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -32,11 +32,16 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QMultiMap>
|
||||
#include <QtCore/QRect>
|
||||
#include <QtCore/QDataStream>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
namespace find_object {
|
||||
|
||||
class ObjSignature {
|
||||
public:
|
||||
ObjSignature() :
|
||||
id_(-1)
|
||||
{}
|
||||
ObjSignature(int id, const cv::Mat & image, const QString & filename) :
|
||||
id_(id),
|
||||
image_(image),
|
||||
@ -44,15 +49,10 @@ public:
|
||||
{}
|
||||
virtual ~ObjSignature() {}
|
||||
|
||||
void setData(const std::vector<cv::KeyPoint> & keypoints,
|
||||
const cv::Mat & descriptors,
|
||||
const QString & detectorType,
|
||||
const QString & descriptorType)
|
||||
void setData(const std::vector<cv::KeyPoint> & keypoints, const cv::Mat & descriptors)
|
||||
{
|
||||
keypoints_ = keypoints;
|
||||
descriptors_ = descriptors;
|
||||
detectorType_ = detectorType;
|
||||
descriptorType_ = descriptorType;
|
||||
}
|
||||
void setWords(const QMultiMap<int, int> & words) {words_ = words;}
|
||||
void setId(int id) {id_ = id;}
|
||||
@ -65,8 +65,69 @@ public:
|
||||
const std::vector<cv::KeyPoint> & keypoints() const {return keypoints_;}
|
||||
const cv::Mat & descriptors() const {return descriptors_;}
|
||||
const QMultiMap<int, int> & words() const {return words_;}
|
||||
const QString & detectorType() const {return detectorType_;}
|
||||
const QString & descriptorType() const {return descriptorType_;}
|
||||
|
||||
void save(QDataStream & streamPtr) const
|
||||
{
|
||||
streamPtr << id_;
|
||||
streamPtr << filename_;
|
||||
streamPtr << (int)keypoints_.size();
|
||||
for(unsigned int j=0; j<keypoints_.size(); ++j)
|
||||
{
|
||||
streamPtr << keypoints_.at(j).angle <<
|
||||
keypoints_.at(j).class_id <<
|
||||
keypoints_.at(j).octave <<
|
||||
keypoints_.at(j).pt.x <<
|
||||
keypoints_.at(j).pt.y <<
|
||||
keypoints_.at(j).response <<
|
||||
keypoints_.at(j).size;
|
||||
}
|
||||
|
||||
qint64 dataSize = descriptors_.elemSize()*descriptors_.cols*descriptors_.rows;
|
||||
streamPtr << descriptors_.rows <<
|
||||
descriptors_.cols <<
|
||||
descriptors_.type() <<
|
||||
dataSize;
|
||||
streamPtr << QByteArray((char*)descriptors_.data, dataSize);
|
||||
|
||||
streamPtr << words_;
|
||||
|
||||
std::vector<unsigned char> bytes;
|
||||
cv::imencode(".png", image_, bytes);
|
||||
streamPtr << QByteArray((char*)bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
void load(QDataStream & streamPtr)
|
||||
{
|
||||
int nKpts;
|
||||
streamPtr >> id_ >> filename_ >> nKpts;
|
||||
keypoints_.resize(nKpts);
|
||||
for(int i=0;i<nKpts;++i)
|
||||
{
|
||||
streamPtr >>
|
||||
keypoints_[i].angle >>
|
||||
keypoints_[i].class_id >>
|
||||
keypoints_[i].octave >>
|
||||
keypoints_[i].pt.x >>
|
||||
keypoints_[i].pt.y >>
|
||||
keypoints_[i].response >>
|
||||
keypoints_[i].size;
|
||||
}
|
||||
|
||||
int rows,cols,type;
|
||||
qint64 dataSize;
|
||||
streamPtr >> rows >> cols >> type >> dataSize;
|
||||
QByteArray data;
|
||||
streamPtr >> data;
|
||||
descriptors_ = cv::Mat(rows, cols, type, data.data()).clone();
|
||||
|
||||
streamPtr >> words_;
|
||||
|
||||
QByteArray image;
|
||||
streamPtr >> image;
|
||||
std::vector<unsigned char> bytes(image.size());
|
||||
memcpy(bytes.data(), image.data(), image.size());
|
||||
image_ = cv::imdecode(bytes, cv::IMREAD_UNCHANGED);
|
||||
}
|
||||
|
||||
private:
|
||||
int id_;
|
||||
@ -75,8 +136,6 @@ private:
|
||||
std::vector<cv::KeyPoint> keypoints_;
|
||||
cv::Mat descriptors_;
|
||||
QMultiMap<int, int> words_; // <word id, keypoint indexes>
|
||||
QString detectorType_;
|
||||
QString descriptorType_;
|
||||
};
|
||||
|
||||
} // namespace find_object
|
||||
|
||||
@ -51,6 +51,41 @@ void Vocabulary::clear()
|
||||
notIndexedWordIds_.clear();
|
||||
}
|
||||
|
||||
void Vocabulary::save(QDataStream & streamPtr) const
|
||||
{
|
||||
if(!indexedDescriptors_.empty() && !wordToObjects_.empty())
|
||||
{
|
||||
UASSERT(notIndexedDescriptors_.empty() && notIndexedWordIds_.empty());
|
||||
|
||||
// save index
|
||||
streamPtr << wordToObjects_;
|
||||
|
||||
// save words
|
||||
qint64 dataSize = indexedDescriptors_.elemSize()*indexedDescriptors_.cols*indexedDescriptors_.rows;
|
||||
streamPtr << indexedDescriptors_.rows <<
|
||||
indexedDescriptors_.cols <<
|
||||
indexedDescriptors_.type() <<
|
||||
dataSize;
|
||||
streamPtr << QByteArray((char*)indexedDescriptors_.data, dataSize);
|
||||
}
|
||||
}
|
||||
|
||||
void Vocabulary::load(QDataStream & streamPtr)
|
||||
{
|
||||
// load index
|
||||
streamPtr >> wordToObjects_;
|
||||
|
||||
// load words
|
||||
int rows,cols,type;
|
||||
qint64 dataSize;
|
||||
streamPtr >> rows >> cols >> type >> dataSize;
|
||||
QByteArray data;
|
||||
streamPtr >> data;
|
||||
indexedDescriptors_ = cv::Mat(rows, cols, type, data.data()).clone();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QMultiMap<int, int> Vocabulary::addWords(const cv::Mat & descriptors, int objectId, bool incremental)
|
||||
{
|
||||
QMultiMap<int, int> words;
|
||||
|
||||
@ -45,6 +45,10 @@ public:
|
||||
void search(const cv::Mat & descriptors, cv::Mat & results, cv::Mat & dists, int k);
|
||||
int size() const {return indexedDescriptors_.rows + notIndexedDescriptors_.rows;}
|
||||
const QMultiMap<int, int> & wordToObjects() const {return wordToObjects_;}
|
||||
const cv::Mat & indexedDescriptors() const {return indexedDescriptors_;}
|
||||
|
||||
void save(QDataStream & streamPtr) const;
|
||||
void load(QDataStream & streamPtr);
|
||||
|
||||
private:
|
||||
cv::flann::Index flannIndex_;
|
||||
|
||||
@ -219,6 +219,9 @@
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="actionLoad_session"/>
|
||||
<addaction name="actionSave_session"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionLoad_objects"/>
|
||||
<addaction name="actionSave_objects"/>
|
||||
<addaction name="separator"/>
|
||||
@ -850,6 +853,16 @@
|
||||
<string>Camera from TCP/IP...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoad_session">
|
||||
<property name="text">
|
||||
<string>Load session...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSave_session">
|
||||
<property name="text">
|
||||
<string>Save session...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionHide_objects_features">
|
||||
<property name="text">
|
||||
<string>Hide objects features</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user