app/main.cpp
Go to the documentation of this file.
1 /*
2 Copyright (c) 2011-2014, Mathieu Labbe - IntRoLab - Universite de Sherbrooke
3 All rights reserved.
4 
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions are met:
7  * Redistributions of source code must retain the above copyright
8  notice, this list of conditions and the following disclaimer.
9  * Redistributions in binary form must reproduce the above copyright
10  notice, this list of conditions and the following disclaimer in the
11  documentation and/or other materials provided with the distribution.
12  * Neither the name of the Universite de Sherbrooke nor the
13  names of its contributors may be used to endorse or promote products
14  derived from this software without specific prior written permission.
15 
16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
20 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27 
28 #include <QApplication>
29 #include <QtCore/QDir>
30 #include <QtCore/QFile>
31 #include <iostream>
32 #include <iomanip>
33 #include "find_object/MainWindow.h"
34 #include "find_object/Settings.h"
35 #include "find_object/FindObject.h"
36 #include "find_object/Camera.h"
37 #include "find_object/TcpServer.h"
38 #include "find_object/JsonWriter.h"
40 #include "TcpServerPool.h"
41 
42 bool running = true;
43 
44 #ifdef WIN32
45 #include <windows.h>
46 BOOL WINAPI my_handler(DWORD signal)
47 {
48  if (signal == CTRL_C_EVENT)
49  {
50  printf("\nCtrl-C caught! Quitting application...\n");
51  QCoreApplication::quit();
52  }
53  return TRUE;
54  running = false;
55 }
56 #else
57 #include <signal.h>
58 void my_handler(int s)
59 {
60  printf("\nCtrl-C caught! Quitting application...\n");
61  QCoreApplication::quit();
62  running = false;
63 }
64 inline void Sleep(unsigned int ms)
65 {
66  struct timespec req;
67  struct timespec rem;
68  req.tv_sec = ms / 1000;
69  req.tv_nsec = (ms - req.tv_sec * 1000) * 1000 * 1000;
70  nanosleep (&req, &rem);
71 }
72 #endif
73 
75 {
76 // Catch ctrl-c to close Qt
77 #ifdef WIN32
78  if (!SetConsoleCtrlHandler(my_handler, TRUE))
79  {
80  UERROR("Could not set control (ctrl-c) handler");
81  }
82 #else
83  struct sigaction sigIntHandler;
84  sigIntHandler.sa_handler = my_handler;
85  sigemptyset(&sigIntHandler.sa_mask);
86  sigIntHandler.sa_flags = 0;
87  sigaction(SIGINT, &sigIntHandler, NULL);
88 #endif
89 }
90 
91 void showUsage()
92 {
93  printf("\nUsage:\n"
94 #ifdef WIN32
95  " Find-Object.exe [options]\n"
96 #else
97  " find_object [options]\n"
98 #endif
99  "Options:\n"
100  " --console Don't use the GUI (by default the camera will be\n"
101  " started automatically). Option --objects must also be\n"
102  " used with valid objects.\n"
103  " --session \"path\" Path to a session to load (*.bin). Use \"--session_new\" to\n"
104  " create a session instead (will be saved to \"path\" on exit, only\n"
105  " on console mode).\n"
106  " --object \"path\" Path to an object to detect.\n"
107  " --objects \"path\" Directory of the objects to detect (--object is ignored).\n"
108  " --config \"path\" Path to configuration file (default: %s).\n"
109  " If set to \"\", default parameters are used\n"
110  " without saving modified parameters on closing.\n"
111  " --scene \"path\" Path to a scene image file.\n"
112  " --vocabulary \"path\" Path to a vocabulary file (*.yaml *.xml). Parameters \"General/invertedSearch\"\n"
113  " and \"General/vocabularyFixed\" will be also enabled. Ignored if \"--session\" is set.\n"
114  " --images_not_saved Don't keep images in RAM after the features are extracted (only\n"
115  " in console mode). Images won't be saved if an output session is set.\n"
116  " --tcp_threads # Number of TCP threads (default 1, only in --console mode). \"--General/port\" parameter should not be 0.\n"
117  " Port numbers start from \"General/port\" value. \"Detect\" TCP service can be\n"
118  " executed at the same time by multiple threads. \"Add/Remove\" TCP services\n"
119  " cannot be called by multiple threads, so calling these services on a port\n "
120  " will block all other threads on the other ports.\n"
121  " --debug Show debug log.\n"
122  " --log-time Show log with time.\n"
123  " --params Show all parameters.\n"
124  " --defaults Use default parameters (--config is ignored).\n"
125  " --My/Parameter \"value\" Set find-Object's parameter (look --params for parameters' name).\n"
126  " It will override the one in --config. Example to set 4 threads:\n"
127  " $ find_object --General/threads 4\n"
128  " --json \"path\" Path to an output JSON file (only in --console mode with --scene).\n"
129  " --help Show usage.\n"
130  , find_object::Settings::iniDefaultPath().toStdString().c_str());
131  exit(-1);
132 }
133 
134 int main(int argc, char* argv[])
135 {
138  ULogger::setPrintWhere(false);
139  ULogger::setPrintTime(false);
140 
142  // parse options BEGIN
144  bool guiMode = true;
145  QString sessionPath = "";
146  bool sessionNew = false;
147  QString objectsPath = "";
148  QString objectPath = "";
149  QString scenePath = "";
150  QString configPath = "";
151  QString vocabularyPath = "";
152  QString jsonPath;
153  find_object::ParametersMap customParameters;
154  bool imagesSaved = true;
155  int tcpThreads = 1;
156 
157  for(int i=1; i<argc; ++i)
158  {
159 #ifdef __APPLE__
160  if(QString(argv[i]).startsWith("-psn"))
161  {
162  //safely ignore
163  continue;
164  }
165 #endif
166  if(strcmp(argv[i], "-objs") == 0 ||
167  strcmp(argv[i], "--objs") == 0 ||
168  strcmp(argv[i], "-objects") == 0 ||
169  strcmp(argv[i], "--objects") == 0)
170  {
171  ++i;
172  if(i < argc)
173  {
174  objectsPath = argv[i];
175  if(objectsPath.contains('~'))
176  {
177  objectsPath.replace('~', QDir::homePath());
178  }
179  if(!QDir(objectsPath).exists())
180  {
181  UERROR("Objects path not valid : %s", objectsPath.toStdString().c_str());
182  showUsage();
183  }
184  }
185  else
186  {
187  showUsage();
188  }
189  continue;
190  }
191  if(strcmp(argv[i], "-session") == 0 ||
192  strcmp(argv[i], "--session") == 0 ||
193  strcmp(argv[i], "-session_new") == 0 ||
194  strcmp(argv[i], "--session_new") == 0)
195  {
196  if(strcmp(argv[i], "-session_new") == 0 ||
197  strcmp(argv[i], "--session_new") == 0)
198  {
199  sessionNew = true;
200  }
201  ++i;
202  if(i < argc)
203  {
204  sessionPath = argv[i];
205  if(sessionPath.contains('~'))
206  {
207  sessionPath.replace('~', QDir::homePath());
208  }
209 
210  if(!sessionNew && !QFile(sessionPath).exists())
211  {
212  UERROR("Session path not valid : %s (if you want to create a new session, use \"--session_new\")", sessionPath.toStdString().c_str());
213  showUsage();
214  }
215  }
216  else
217  {
218  showUsage();
219  }
220  continue;
221  }
222  if(strcmp(argv[i], "-object") == 0 ||
223  strcmp(argv[i], "--object") == 0)
224  {
225  ++i;
226  if(i < argc)
227  {
228  objectPath = argv[i];
229  if(objectPath.contains('~'))
230  {
231  objectPath.replace('~', QDir::homePath());
232  }
233  if(!QFile(objectPath).exists())
234  {
235  UERROR("Object path not valid : %s", objectPath.toStdString().c_str());
236  showUsage();
237  }
238  }
239  else
240  {
241  showUsage();
242  }
243  continue;
244  }
245  if(strcmp(argv[i], "-scene") == 0 ||
246  strcmp(argv[i], "--scene") == 0)
247  {
248  ++i;
249  if(i < argc)
250  {
251  scenePath = argv[i];
252  if(scenePath.contains('~'))
253  {
254  scenePath.replace('~', QDir::homePath());
255  }
256  if(!QFile(scenePath).exists())
257  {
258  UERROR("Scene path not valid : %s", scenePath.toStdString().c_str());
259  showUsage();
260  }
261  }
262  else
263  {
264  showUsage();
265  }
266  continue;
267  }
268  if(strcmp(argv[i], "-vocabulary") == 0 ||
269  strcmp(argv[i], "--vocabulary") == 0)
270  {
271  ++i;
272  if(i < argc)
273  {
274  vocabularyPath = argv[i];
275  if(vocabularyPath.contains('~'))
276  {
277  vocabularyPath.replace('~', QDir::homePath());
278  }
279  if(!QFile(vocabularyPath).exists())
280  {
281  UERROR("Vocabulary path not valid : %s", vocabularyPath.toStdString().c_str());
282  showUsage();
283  }
284  }
285  else
286  {
287  showUsage();
288  }
289  continue;
290  }
291  if(strcmp(argv[i], "-config") == 0 ||
292  strcmp(argv[i], "--config") == 0)
293  {
294  ++i;
295  if(i < argc)
296  {
297  configPath = argv[i];
298  if(configPath.contains('~'))
299  {
300  configPath.replace('~', QDir::homePath());
301  }
302  if(!configPath.isEmpty() && !QFile::exists(configPath))
303  {
304  UWARN("Configuration file \"%s\" doesn't exist, it will be created with default values...", configPath.toStdString().c_str());
305  }
306  }
307  else
308  {
309  showUsage();
310  }
311  continue;
312  }
313  if(strcmp(argv[i], "-console") == 0 ||
314  strcmp(argv[i], "--console") == 0)
315  {
316  guiMode = false;
317  continue;
318  }
319  if(strcmp(argv[i], "-images_not_saved") == 0 ||
320  strcmp(argv[i], "--images_not_saved") == 0)
321  {
322  imagesSaved = false;
323  continue;
324  }
325  if(strcmp(argv[i], "-debug") == 0 ||
326  strcmp(argv[i], "--debug") == 0)
327  {
328  customParameters.insert(find_object::Settings::kGeneral_debug(), true);
329  continue;
330  }
331  if(strcmp(argv[i], "-log-time") == 0 ||
332  strcmp(argv[i], "--log-time") == 0)
333  {
335  ULogger::setPrintTime(true);
336  continue;
337  }
338  if(strcmp(argv[i], "-help") == 0 ||
339  strcmp(argv[i], "--help") == 0)
340  {
341  showUsage();
342  }
343  if(strcmp(argv[i], "-json") == 0 ||
344  strcmp(argv[i], "--json") == 0)
345  {
346  ++i;
347  if(i < argc)
348  {
349  jsonPath = argv[i];
350  if(jsonPath.contains('~'))
351  {
352  jsonPath.replace('~', QDir::homePath());
353  }
354  }
355  else
356  {
357  showUsage();
358  }
359  continue;
360  }
361  if(strcmp(argv[i], "-tcp_threads") == 0 ||
362  strcmp(argv[i], "--tcp_threads") == 0)
363  {
364  ++i;
365  if(i < argc)
366  {
367  tcpThreads = atoi(argv[i]);
368  if(tcpThreads < 1)
369  {
370  printf("tcp_threads should be >= 1!\n");
371  showUsage();
372  }
373  }
374  else
375  {
376  showUsage();
377  }
378  continue;
379  }
380  if(strcmp(argv[i], "--params") == 0)
381  {
383  for(find_object::ParametersMap::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter)
384  {
385  std::string str = "Param: " + iter.key().toStdString() + " = \"" + iter.value().toString().toStdString() + "\"";
386  std::cout <<
387  str <<
388  std::setw(60 - str.size()) <<
389  " [" <<
390  find_object::Settings::getDescriptions().value(iter.key()).toStdString().c_str() <<
391  "]" <<
392  std::endl;
393  }
394  UINFO("Node will now exit after showing default Find-Object's parameters because "
395  "argument \"--params\" is detected!");
396  exit(0);
397  }
398 
399  // Check for custom parameters:
401  QString name = argv[i];
402  if(name.size() > 2)
403  {
404  //strip the "--"
405  name.remove(0, 2);
406  if(parameters.contains(name))
407  {
408  ++i;
409  if(i < argc)
410  {
411  customParameters.insert(name, argv[i]);
412  }
413  else
414  {
415  showUsage();
416  }
417  continue;
418  }
419  }
420 
421  UERROR("Unrecognized option : %s", argv[i]);
422  showUsage();
423  }
424 
425  UINFO("Options:");
426  UINFO(" GUI mode = %s", guiMode?"true":"false");
427  if(!sessionPath.isEmpty())
428  {
429  if(sessionNew)
430  {
431  UINFO(" Session path: \"%s\" [NEW]", sessionPath.toStdString().c_str());
432  if(configPath.isEmpty() && guiMode)
433  {
435  }
436  }
437  else
438  {
439  UINFO(" Session path: \"%s\"", sessionPath.toStdString().c_str());
440  if(!vocabularyPath.isEmpty())
441  {
442  UWARN("Vocabulary \"%s\" is not loaded as a session \"%s\" is already "
443  "loaded, ignoring vocabulary file...",
444  vocabularyPath.toStdString().c_str(),
445  sessionPath.toStdString().c_str());
446  vocabularyPath.clear();
447  }
448  if(!configPath.isEmpty())
449  {
450  UWARN("A session \"%s\" is loaded and a config file is also used, "
451  "the parameters of the session will be overwritten by "
452  "those in the config file \"%s\".",
453  sessionPath.toStdString().c_str(),
454  configPath.toStdString().c_str());
455  }
456  }
457  }
458  else if(configPath.isEmpty() && guiMode)
459  {
461  }
462  if(!objectsPath.isEmpty())
463  {
464  UINFO(" Objects path: \"%s\"", objectsPath.toStdString().c_str());
465  }
466  else if(!objectPath.isEmpty())
467  {
468  UINFO(" Object path: \"%s\"", objectPath.toStdString().c_str());
469  }
470  UINFO(" Scene path: \"%s\"", scenePath.toStdString().c_str());
471  if(!guiMode)
472  {
473  UINFO(" JSON path: \"%s\"", jsonPath.toStdString().c_str());
474  }
475  UINFO(" Settings path: \"%s\"", configPath.toStdString().c_str());
476  UINFO(" Vocabulary path: \"%s\"", vocabularyPath.toStdString().c_str());
477 
478  if(!vocabularyPath.isEmpty())
479  {
480  if(customParameters.contains(find_object::Settings::kGeneral_vocabularyFixed()))
481  {
482  UWARN("\"General/vocabularyFixed\" custom parameter overwritten as a fixed vocabulary is used.");
483  }
484  if(customParameters.contains(find_object::Settings::kGeneral_invertedSearch()))
485  {
486  UWARN("\"General/invertedSearch\" custom parameter overwritten as a fixed vocabulary is used.");
487  }
488  customParameters[find_object::Settings::kGeneral_vocabularyFixed()] = true;
489  customParameters[find_object::Settings::kGeneral_invertedSearch()] = true;
490  }
491 
492  for(find_object::ParametersMap::iterator iter= customParameters.begin(); iter!=customParameters.end(); ++iter)
493  {
494  UINFO(" Param \"%s\"=\"%s\"", iter.key().toStdString().c_str(), iter.value().toString().toStdString().c_str());
495  }
496 
498  // parse options END
500 
501  // Load settings, should be loaded before creating other objects
502  find_object::ParametersMap parameters;
503  if(!configPath.isEmpty())
504  {
505  parameters = find_object::Settings::init(configPath);
506  }
507 
508  // Override custom parameters:
509  for(find_object::ParametersMap::iterator iter= customParameters.begin(); iter!=customParameters.end(); ++iter)
510  {
511  find_object::Settings::setParameter(iter.key(), iter.value());
512  parameters.insert(iter.key(), iter.value());
513  }
514 
515 
516  // Create FindObject
517  find_object::FindObject * findObject = new find_object::FindObject(guiMode || imagesSaved);
518 
519  // Load objects if path is set
520  int objectsLoaded = 0;
521  if(!sessionPath.isEmpty() && !sessionNew)
522  {
523  if(!findObject->loadSession(sessionPath, parameters))
524  {
525  UERROR("Could not load session \"%s\"", sessionPath.toStdString().c_str());
526  }
527  else
528  {
529  objectsLoaded = findObject->objects().size();
530  }
531  }
532  else if(!vocabularyPath.isEmpty() && !findObject->loadVocabulary(vocabularyPath))
533  {
534  UERROR("Failed to load vocabulary \"%s\"", vocabularyPath.toStdString().c_str());
535  }
536 
537  if(!objectsPath.isEmpty())
538  {
539  if(!vocabularyPath.isEmpty() && !findObject->loadVocabulary(vocabularyPath))
540  {
541  UERROR("Failed to load vocabulary \"%s\"", vocabularyPath.toStdString().c_str());
542  }
543  objectsLoaded = findObject->loadObjects(objectsPath);
544  if(!objectsLoaded)
545  {
546  UWARN("No objects loaded from \"%s\"", objectsPath.toStdString().c_str());
547  }
548  }
549  else if(!objectPath.isEmpty())
550  {
551  if(!vocabularyPath.isEmpty() && !findObject->loadVocabulary(vocabularyPath))
552  {
553  UERROR("Failed to load vocabulary \"%s\"", vocabularyPath.toStdString().c_str());
554  }
555 
556  const find_object::ObjSignature * obj = findObject->addObject(objectPath);
557  if(obj)
558  {
559  ++objectsLoaded;
560  findObject->updateObjects();
561  findObject->updateVocabulary();
562  }
563  else
564  {
565  UWARN("No object loaded from \"%s\"", objectsPath.toStdString().c_str());
566  }
567  }
568 
569  cv::Mat scene;
570  if(!scenePath.isEmpty())
571  {
572  scene = cv::imread(scenePath.toStdString());
573  if(scene.empty())
574  {
575  UERROR("Failed to load scene \"%s\"", scenePath.toStdString().c_str());
576  }
577  }
578 
579  if(guiMode)
580  {
581  QApplication app(argc, argv);
582  find_object::MainWindow mainWindow(findObject, 0); // ownership transfered
583 
584  app.connect( &app, SIGNAL( lastWindowClosed() ), &app, SLOT( quit() ) );
585  mainWindow.show();
586 
587  if(!scene.empty())
588  {
589  mainWindow.update(scene);
590  }
591 
592  app.exec();
593 
594  // Save settings
596  }
597  else
598  {
599  QCoreApplication app(argc, argv);
600 
601  if(!scene.empty())
602  {
603  // process the scene and exit
604  QTime time;
605  time.start();
607  findObject->detect(scene, info);
608 
609  if(info.objDetected_.size() > 1)
610  {
611  UINFO("%d objects detected! (%d ms)", (int)info.objDetected_.size(), time.elapsed());
612  }
613  else if(info.objDetected_.size() == 1)
614  {
615  UINFO("Object %d detected! (%d ms)", (int)info.objDetected_.begin().key(), time.elapsed());
616  }
617  else if(find_object::Settings::getGeneral_sendNoObjDetectedEvents())
618  {
619  UINFO("No objects detected. (%d ms)", time.elapsed());
620  }
621 
622  if(!jsonPath.isEmpty())
623  {
624  find_object::JsonWriter::write(info, jsonPath);
625  UINFO("JSON written to \"%s\"", jsonPath.toStdString().c_str());
626  }
627  }
628  else
629  {
630  TcpServerPool tcpServerPool(findObject, tcpThreads, find_object::Settings::getGeneral_port());
631 
632  setupQuitSignal();
633 
634  //If TCP camera is used
635  find_object::Camera * camera = 0;
636  if(find_object::Settings::getCamera_6useTcpCamera())
637  {
638  camera = new find_object::Camera();
639 
640  // [Camera] ---Image---> [FindObject]
641  QObject::connect(camera, SIGNAL(imageReceived(const cv::Mat &)), findObject, SLOT(detect(const cv::Mat &)));
642  QObject::connect(camera, SIGNAL(finished()), &app, SLOT(quit()));
643 
644  if(!camera->start())
645  {
646  UERROR("Camera initialization failed!");
647  running = false;
648  }
649  }
650 
651  // start processing!
652  if(running)
653  {
654  app.exec();
655 
656  if(!sessionPath.isEmpty())
657  {
658  if(findObject->isSessionModified())
659  {
660  UINFO("The session has been modified, updating the session file...");
661  if(findObject->saveSession(sessionPath))
662  {
663  UINFO("Session \"%s\" successfully saved (%d objects)!",
664  sessionPath.toStdString().c_str(), findObject->objects().size());
665  }
666  }
667  else if(sessionNew)
668  {
669  UINFO("The session has not been modified, session file not created...");
670  }
671  }
672  }
673 
674  // cleanup
675  if(camera)
676  {
677  camera->stop();
678  delete camera;
679  }
680  }
681 
682  delete findObject;
683  }
684  return 0;
685 }
app
static QString iniDefaultPath()
Definition: Settings.cpp:83
virtual bool start()
Definition: Camera.cpp:186
void setupQuitSignal()
Definition: app/main.cpp:74
bool detect(const cv::Mat &image, find_object::DetectionInfo &info) const
static void setLevel(ULogger::Level level)
Definition: ULogger.h:301
bool loadSession(const QString &path, const ParametersMap &customParameters=ParametersMap())
Definition: FindObject.cpp:82
void updateVocabulary(const QList< int > &ids=QList< int >())
Definition: FindObject.cpp:950
static void setPrintTime(bool printTime)
Definition: ULogger.h:242
int loadObjects(const QString &dirPath, bool recursive=false)
Definition: FindObject.cpp:236
const QMap< int, ObjSignature * > & objects() const
Definition: FindObject.h:88
bool loadVocabulary(const QString &filePath)
Definition: FindObject.cpp:196
static void setType(Type type, const std::string &fileName=kDefaultLogFileName, bool append=true)
Definition: ULogger.cpp:175
void showUsage()
Definition: app/main.cpp:91
QMultiMap< int, QTransform > objDetected_
Definition: DetectionInfo.h:71
void updateObjects(const QList< int > &ids=QList< int >())
Definition: FindObject.cpp:856
static void setParameter(const QString &key, const QVariant &value)
Definition: Settings.h:341
static const DescriptionsMap & getDescriptions()
Definition: Settings.h:340
QMap< QString, QVariant > ParametersMap
Definition: Settings.h:41
void update(const cv::Mat &image)
static void setPrintWhere(bool printWhere)
Definition: ULogger.h:271
void Sleep(unsigned int ms)
Definition: app/main.cpp:64
void my_handler(int s)
Definition: app/main.cpp:58
virtual void stop()
Definition: Camera.cpp:58
bool running
Definition: app/main.cpp:42
bool saveSession(const QString &path)
Definition: FindObject.cpp:144
#define UERROR(...)
ULogger class and convenient macros.
static void write(const DetectionInfo &info, const QString &path)
Definition: JsonWriter.cpp:39
#define UWARN(...)
static const ParametersMap & getDefaultParameters()
Definition: Settings.h:337
ROSCPP_DECL bool exists(const std::string &service_name, bool print_failure_reason)
int main(int argc, char *argv[])
Definition: app/main.cpp:134
static ParametersMap init(const QString &fileName)
Definition: Settings.cpp:101
static void saveSettings(const QString &fileName=QString())
Definition: Settings.cpp:288
bool isSessionModified() const
Definition: FindObject.h:70
const ObjSignature * addObject(const QString &filePath)
Definition: FindObject.cpp:283
#define UINFO(...)


find_object_2d
Author(s): Mathieu Labbe
autogenerated on Mon Dec 12 2022 03:20:09