├── CMakeLists.txt ├── README.md ├── cameraapp.cpp ├── cameragridframe.cpp ├── cameragridframe.h ├── camerapanel.cpp ├── camerapanel.h ├── camerathread.cpp ├── camerathread.h ├── convertmattowxbmp.cpp ├── convertmattowxbmp.h ├── onecameraframe.cpp ├── onecameraframe.h └── screenshots └── wxopencvcameras.png /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | ## Name: CMakeLists.txt 3 | ## Purpose: To build wxOpenCVCameras application 4 | ## Author: PB 5 | ## Created: 2021-11-18 6 | ## Copyright: (c) 2021 PB 7 | ## Licence: wxWindows licence 8 | ############################################################################### 9 | 10 | cmake_minimum_required(VERSION 3.1 FATAL_ERROR) 11 | project(wxOpenCVCameras) 12 | 13 | find_package(wxWidgets 3.1.0 COMPONENTS core base REQUIRED) 14 | find_package(OpenCV 4.2 REQUIRED) 15 | 16 | set(SOURCES 17 | cameragridframe.h 18 | camerapanel.h 19 | camerathread.h 20 | convertmattowxbmp.h 21 | onecameraframe.h 22 | cameraapp.cpp 23 | cameragridframe.cpp 24 | camerapanel.cpp 25 | camerathread.cpp 26 | convertmattowxbmp.cpp 27 | onecameraframe.cpp 28 | ) 29 | 30 | if (WIN32) 31 | list(APPEND SOURCES "${wxWidgets_ROOT_DIR}/include/wx/msw/wx.rc") 32 | endif() 33 | 34 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 35 | set_property (DIRECTORY PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME}) 36 | 37 | add_executable(${PROJECT_NAME} ${SOURCES}) 38 | 39 | include(${wxWidgets_USE_FILE}) 40 | 41 | set_target_properties(${PROJECT_NAME} PROPERTIES 42 | CXX_STANDARD 11 43 | CXX_STANDARD_REQUIRED YES 44 | ) 45 | 46 | if (WIN32) 47 | target_compile_definitions(${PROJECT_NAME} PRIVATE 48 | wxUSE_RC_MANIFEST 49 | wxUSE_DPI_AWARE_MANIFEST=2) 50 | set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE YES) 51 | 52 | if (MSVC) 53 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO") 54 | endif(MSVC) 55 | 56 | elseif(APPLE) 57 | set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE YES) 58 | endif() 59 | 60 | target_link_libraries(${PROJECT_NAME} PRIVATE ${wxWidgets_LIBRARIES} ${OpenCV_LIBS}) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | --------- 3 | 4 | wxOpenCVCameras is a crude untested example of how to retrieve and display 5 | images from multiple cameras, using OpenCV to grab images from a camera 6 | and wxWidgets to display the images. 7 | 8 | ![wxOpenCVCameras Screenshot](screenshots/wxopencvcameras.png?raw=true) 9 | 10 | Every camera has its own worker thread `CameraThread`, which grabs a frame 11 | from a camera with `cv::VideoCapture`, converts the frame from `cv::Mat` 12 | to `wxBitmap`, and creates a resized thumbnail also converted to `wxBitmap`. 13 | Additionally, benchmarking data (times for grabbing image, converting 14 | it to `wxBitmap`, creating a thumbnail...) are collected. 15 | 16 | The full resolution bitmap, the thumbnail bitmap, and the benchmarking data 17 | are stored in `CameraFrameData` class. As `wxBitmap` is implemented as 18 | copy-on-write in a probably thread-unsafe manner, the `wxBitmap`s are stored 19 | as raw pointers and `CameraFrameData`s in `std::unique_ptr`. 20 | 21 | `CameraFrameData` is then added by a worker thread to a container 22 | shared between the GUI thread and camera threads. The container is `std::vector` 23 | (see `CameraGridFrame::m_newCameraFrameData` and `CameraThread::CameraSetupData::frames`) protected 24 | by `wxCriticalSection` (see `CameraGridFrame::m_newCameraFrameDataCS` 25 | and `CameraThread::CameraSetupData::framesCS`). The GUI thread (a `wxFrame`-derived `CameraGridFrame`) 26 | then uses a fixed-frequency `wxTimer` to update the camera display with images stored 27 | in the container. 28 | 29 | The GUI has a crude control of the camera (thread) by using `wxMessageQueue` to pass 30 | the commands (such as setting the thread sleep time or getting/setting one of 31 | `cv::VideoCaptureProperties`) from the GUI to the camera thread. 32 | 33 | GUI 34 | --------- 35 | A camera can be added either as an integer (e.g., `0` for a default webcam) or as an URL. 36 | There is a couple of preset camera URLs offered via a menu, but these are not guaranteed 37 | to stay online and are mostly time-limited. When a camera is added, settings from menu 38 | "Defaults for New Cameras" are used. 39 | 40 | Output from all cameras is displayed in a single frame as thumbnails (`CameraPanel`s 41 | in a `wxWrapSizer`). Left doubleclicking a thumbnail opens a new frame (`OneCameraFrame`) 42 | showing the camera output in the full resolution. Right clicking a thumbnail shows 43 | a popup menu allowing crude communication with the camera (thread). 44 | 45 | In the debug build, various diagnostic messages are output with `wxLogTrace(TRACE_WXOPENCVCAMERAS, ...)`. 46 | 47 | Notes 48 | --------- 49 | wxOpenCVCameras uses internet streams as camera sources. If an application connects to multiple 50 | hardware cameras with the same parameters, an entirely different approach should probably be used. 51 | For example, a single worker thread for capturing from all the cameras, using `cv::VideoCapture::waitAny()` 52 | with `cv::VideoCapture::retrieve()` and one or more worker threads for processing the captured 53 | images. Such approach would be not only less thread-hungry, the frames from multiple cameras 54 | should be better synchronized as well. 55 | 56 | To convert `cv::Mat` to `wxBitmap`, the code uses `ConvertMatBitmapTowxBitmap()` from the 57 | [wxOpenCVTest project](https://github.com/PBfordev/wxopencvtest), so all the information 58 | provided there applies here is as well. 59 | 60 | Removing a camera (i.e., stopping a thread) may sometimes take a while so that the program 61 | appears to be stuck. However, this happens when the worker thread is stuck in an OpenCV call 62 | (e.g., opening/closing `cv::VideoCapture` or grabbing the image) that may sometimes take a while, 63 | where the thread cannot test whether to stop. 64 | 65 | Requirements 66 | --------- 67 | Requires OpenCV v4.2+, wxWidgets v3.1+, and a compiler supporting C++11 standard. 68 | 69 | Licence 70 | --------- 71 | [wxWidgets licence](https://github.com/wxWidgets/wxWidgets/blob/master/docs/licence.txt) -------------------------------------------------------------------------------- /cameraapp.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: camerapp.cpp 3 | // Purpose: Application class, just creates and shows the main frame 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | #include 11 | 12 | #include "cameragridframe.h" 13 | 14 | class wxOpenCVCamerasApp : public wxApp 15 | { 16 | public: 17 | bool OnInit() override 18 | { 19 | SetVendorName("PB"); 20 | SetAppName("wxOpenCVCameras"); 21 | 22 | (new CameraGridFrame)->Show(); 23 | return true; 24 | } 25 | }; wxIMPLEMENT_APP(wxOpenCVCamerasApp); -------------------------------------------------------------------------------- /cameragridframe.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: cameragridframe.cpp 3 | // Purpose: Displays thumbnails from multiple cameras 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | #include "cameragridframe.h" 21 | #include "camerapanel.h" 22 | #include "camerathread.h" 23 | #include "convertmattowxbmp.h" 24 | #include "onecameraframe.h" 25 | 26 | // based on wxCHECK_VERSION 27 | #define CHECK_OPENCV_VERSION(major,minor,revision) \ 28 | (CV_VERSION_MAJOR > (major) || \ 29 | (CV_VERSION_MAJOR == (major) && CV_VERSION_MINOR > (minor)) || \ 30 | (CV_VERSION_MAJOR == (major) && CV_VERSION_MINOR == (minor) && CV_VERSION_REVISION >= (revision))) 31 | 32 | 33 | // some/most are time-limited 34 | const char* const knownCameraAdresses[] = 35 | { 36 | "http://pendelcam.kip.uni-heidelberg.de/mjpg/video.mjpg", 37 | "https://www.rmp-streaming.com/media/big-buck-bunny-360p.mp4", 38 | "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4", 39 | "http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8", 40 | "http://103.199.161.254/Content/bbcworld/Live/Channel(BBCworld)/index.m3u8", 41 | "https://c.mjh.nz/101002210221/", 42 | }; 43 | 44 | CameraGridFrame::CameraGridFrame(wxWindow* parent) : wxFrame(parent, wxID_ANY, "wxOpenCVCameras") 45 | { 46 | wxMenuBar* menuBar = new wxMenuBar(); 47 | 48 | wxMenu* addOrRemovecameraMenu = new wxMenu; 49 | 50 | addOrRemovecameraMenu->Append(ID_CAMERA_ADD_CUSTOM, "&Add custom..."); 51 | addOrRemovecameraMenu->AppendSeparator(); 52 | addOrRemovecameraMenu->Append(ID_CAMERA_ADD_DEFAULT_WEBCAM, "Add Default &Webcam"); 53 | addOrRemovecameraMenu->AppendSeparator(); 54 | addOrRemovecameraMenu->Append(wxID_FILE1, "Add Pendulum"); 55 | addOrRemovecameraMenu->Append(wxID_FILE2, "Add Bunny 1"); 56 | addOrRemovecameraMenu->Append(wxID_FILE3, "Add Bunny 2"); 57 | addOrRemovecameraMenu->Append(wxID_FILE4, "Add Apple Stream"); 58 | addOrRemovecameraMenu->Append(wxID_FILE5, "Add BBC World"); 59 | addOrRemovecameraMenu->Append(wxID_FILE6, "Add ABC Live"); 60 | addOrRemovecameraMenu->Append(ID_CAMERA_ADD_ALL_IP_ABOVE, "Add All IP Cameras Above\tCtrl+A"); 61 | addOrRemovecameraMenu->AppendSeparator(); 62 | addOrRemovecameraMenu->Append(ID_CAMERA_ADD_DOLBYVISIONHLS, "Add DolbyVision HLS (4k at 60fps)"); 63 | addOrRemovecameraMenu->AppendSeparator(); 64 | addOrRemovecameraMenu->Append(ID_CAMERA_REMOVE, "Remove..."); 65 | addOrRemovecameraMenu->Append(ID_CAMERA_REMOVE_ALL, "&Remove All\tCtrl+R"); 66 | menuBar->Append(addOrRemovecameraMenu, "&Add/Remove Camera"); 67 | 68 | 69 | wxMenu* defaultCameraSettingsMenu = new wxMenu; 70 | wxMenu* threadSleepMenu = new wxMenu; 71 | 72 | defaultCameraSettingsMenu->Append(ID_CAMERA_SET_DEFAULTS_BACKEND, "Set Default &Backend..."); 73 | threadSleepMenu->AppendRadioItem(ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_FROM_FPS, "Based on Camera FPS"); 74 | threadSleepMenu->AppendRadioItem(ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_NONE, "No Sleep"); 75 | threadSleepMenu->AppendRadioItem(ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_CUSTOM, "Custom"); 76 | threadSleepMenu->Append(ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_CUSTOM_SET, "Set Custom Duration..."); 77 | defaultCameraSettingsMenu->AppendSubMenu(threadSleepMenu, "CameraThread Sleep Duration"); 78 | defaultCameraSettingsMenu->Append(ID_CAMERA_SET_DEFAULTS_RESOLUTION, "Resolution..."); 79 | defaultCameraSettingsMenu->Append(ID_CAMERA_SET_DEFAULTS_FPS, "FPS..."); 80 | defaultCameraSettingsMenu->AppendCheckItem(ID_CAMERA_SET_DEFAULTS_USE_MJPEG_FOURCC, "Use MJPEG FourCC"); 81 | defaultCameraSettingsMenu->AppendSeparator(); 82 | defaultCameraSettingsMenu->Append(ID_CAMERA_SET_DEFAULTS_RESET, "&Reset"); 83 | 84 | menuBar->Append(defaultCameraSettingsMenu, "&Defaults for New Cameras"); 85 | 86 | SetMenuBar(menuBar); 87 | 88 | CreateStatusBar(2); 89 | 90 | SetClientSize(900, 700); 91 | 92 | SetSizer(new wxWrapSizer(wxHORIZONTAL)); 93 | 94 | Bind(wxEVT_MENU, &CameraGridFrame::OnAddCamera, this, ID_CAMERA_ADD_CUSTOM); 95 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera("0"); }, ID_CAMERA_ADD_DEFAULT_WEBCAM); 96 | 97 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera(knownCameraAdresses[0]); }, wxID_FILE1); 98 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera(knownCameraAdresses[1]); }, wxID_FILE2); 99 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera(knownCameraAdresses[2]); }, wxID_FILE3); 100 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera(knownCameraAdresses[3]); }, wxID_FILE4); 101 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera(knownCameraAdresses[4]); }, wxID_FILE5); 102 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera(knownCameraAdresses[5]); }, wxID_FILE6); 103 | Bind(wxEVT_MENU, &CameraGridFrame::OnAddAllIPCamerasAbove, this, ID_CAMERA_ADD_ALL_IP_ABOVE); 104 | Bind(wxEVT_MENU, [this](wxCommandEvent&) { AddCamera("http://d3rlna7iyyu8wu.cloudfront.net/DolbyVision_Atmos/profile5_HLS/master.m3u8"); }, ID_CAMERA_ADD_DOLBYVISIONHLS); 105 | 106 | Bind(wxEVT_MENU, &CameraGridFrame::OnRemoveCamera, this, ID_CAMERA_REMOVE); 107 | Bind(wxEVT_MENU, &CameraGridFrame::OnRemoveAllCameras, this, ID_CAMERA_REMOVE_ALL); 108 | 109 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultBackend, this, ID_CAMERA_SET_DEFAULTS_BACKEND); 110 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultThreadSleepFromFPS, this, ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_FROM_FPS); 111 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultThreadSleepNone, this, ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_NONE); 112 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultThreadSleepCustom, this, ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_CUSTOM); 113 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultThreadSleepCustomSetDuration, this, ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_CUSTOM_SET); 114 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultResolution, this, ID_CAMERA_SET_DEFAULTS_RESOLUTION); 115 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultFPS, this, ID_CAMERA_SET_DEFAULTS_FPS); 116 | Bind(wxEVT_MENU, &CameraGridFrame::OnSetCameraDefaultUseMJPGFourCC, this, ID_CAMERA_SET_DEFAULTS_USE_MJPEG_FOURCC); 117 | 118 | Bind(wxEVT_MENU, &CameraGridFrame::OnCameraDefaultsReset, this, ID_CAMERA_SET_DEFAULTS_RESET); 119 | 120 | 121 | m_processNewCameraFrameDataTimer.Start(m_processNewCameraFrameDataInterval); 122 | m_processNewCameraFrameDataTimer.Bind(wxEVT_TIMER, &CameraGridFrame::OnProcessNewCameraFrameData, this); 123 | 124 | Bind(EVT_CAMERA_CAPTURE_STARTED, &CameraGridFrame::OnCameraCaptureStarted, this); 125 | Bind(EVT_CAMERA_COMMAND_RESULT, &CameraGridFrame::OnCameraCommandResult, this); 126 | Bind(EVT_CAMERA_ERROR_OPEN, &CameraGridFrame::OnCameraErrorOpen, this); 127 | Bind(EVT_CAMERA_ERROR_EMPTY, &CameraGridFrame::OnCameraErrorEmpty, this); 128 | Bind(EVT_CAMERA_ERROR_EXCEPTION, &CameraGridFrame::OnCameraErrorException, this); 129 | 130 | m_updateInfoTimer.Bind(wxEVT_TIMER, &CameraGridFrame::OnUpdateInfo, this); 131 | m_updateInfoTimer.Start(1000); // once a second 132 | 133 | wxLog::AddTraceMask(TRACE_WXOPENCVCAMERAS); 134 | 135 | CallAfter([] { wxMessageBox("When a camera thumbnail shows it is receiving, you can:\n (1) Double click it to show the full frame.\n (2) Right click it to communicate with the camera."); } ); 136 | } 137 | 138 | CameraGridFrame::~CameraGridFrame() 139 | { 140 | RemoveAllCameras(); 141 | } 142 | 143 | void CameraGridFrame::OnAddCamera(wxCommandEvent&) 144 | { 145 | static wxString address; 146 | 147 | address = wxGetTextFromUser("Enter either a camera index as unsigned long or as an URL including protocol, address, port etc.", 148 | "Camera", address, this); 149 | 150 | if ( !address.empty() ) 151 | AddCamera(address); 152 | } 153 | 154 | void CameraGridFrame::OnAddAllIPCamerasAbove(wxCommandEvent&) 155 | { 156 | wxWindowUpdateLocker locker; 157 | 158 | for ( const auto& address : knownCameraAdresses ) 159 | AddCamera(address); 160 | 161 | Layout(); 162 | } 163 | 164 | 165 | void CameraGridFrame::OnRemoveCamera(wxCommandEvent&) 166 | { 167 | if ( m_cameras.empty() ) 168 | { 169 | wxLogMessage("No cameras to remove."); 170 | return; 171 | } 172 | 173 | wxArrayString cameras; 174 | wxArrayInt camerasToRemove; 175 | 176 | for ( auto& camera : m_cameras ) 177 | cameras.push_back(camera.second.thread->GetCameraName()); 178 | 179 | if ( wxGetSelectedChoices(camerasToRemove, "Remove camera(s)", 180 | "Select camera(s) to remove", cameras, this) == - 1 181 | ) 182 | { 183 | return; 184 | } 185 | 186 | for ( const auto& cr : camerasToRemove ) 187 | RemoveCamera(cameras[cr]); 188 | } 189 | 190 | void CameraGridFrame::OnRemoveAllCameras(wxCommandEvent&) 191 | { 192 | RemoveAllCameras(); 193 | } 194 | 195 | void CameraGridFrame::OnSetCameraDefaultBackend(wxCommandEvent&) 196 | { 197 | using namespace cv; 198 | 199 | const std::vector vcCameraAPIs = videoio_registry::getCameraBackends(); 200 | const std::vector vcStreamAPIs = videoio_registry::getStreamBackends(); 201 | wxArrayString APINames; 202 | std::vector APIIds; 203 | int initialSelection = -1; 204 | int selectedIndex = -1; 205 | 206 | APINames.push_back(""); 207 | APIIds.push_back(CAP_ANY); 208 | 209 | for ( const auto& api : vcCameraAPIs ) 210 | { 211 | APINames.push_back(videoio_registry::getBackendName(api)); 212 | APIIds.push_back(api); 213 | } 214 | 215 | for ( const auto& api : vcStreamAPIs ) 216 | { 217 | const wxString name(videoio_registry::getBackendName(api)); 218 | 219 | if ( APINames.Index(name) != wxNOT_FOUND ) 220 | continue; 221 | 222 | APINames.push_back(name); 223 | APIIds.push_back(api); 224 | } 225 | 226 | for ( size_t i = 0; i < APIIds.size(); ++i ) 227 | { 228 | if ( APIIds[i] == m_defaultCameraBackend ) 229 | { 230 | initialSelection = i; 231 | break; 232 | } 233 | } 234 | 235 | selectedIndex = wxGetSingleChoiceIndex("Available Backends\n(Camera and Stream)", "Select default VideoCapture backend", APINames, initialSelection, this); 236 | 237 | if ( selectedIndex == -1 ) 238 | return; 239 | 240 | m_defaultCameraBackend = APIIds[selectedIndex]; 241 | } 242 | 243 | 244 | void CameraGridFrame::OnSetCameraDefaultThreadSleepFromFPS(wxCommandEvent&) 245 | { 246 | m_defaultCameraThreadSleepDuration = CameraSetupData::SleepFromFPS; 247 | } 248 | 249 | void CameraGridFrame::OnSetCameraDefaultThreadSleepNone(wxCommandEvent&) 250 | { 251 | m_defaultCameraThreadSleepDuration = CameraSetupData::SleepNone; 252 | } 253 | 254 | void CameraGridFrame::OnSetCameraDefaultThreadSleepCustom(wxCommandEvent&) 255 | { 256 | m_defaultCameraThreadSleepDuration = m_defaultCameraThreadSleepDurationInMs; 257 | } 258 | 259 | void CameraGridFrame::OnSetCameraDefaultThreadSleepCustomSetDuration(wxCommandEvent&) 260 | { 261 | long duration = wxGetNumberFromUser("Sleep duration in ms", "Number between 5 and 1000", 262 | "Select default CameraThread sleep duration", 263 | m_defaultCameraThreadSleepDurationInMs, 264 | 5, 1000, this); 265 | 266 | if ( duration == -1 ) 267 | return; 268 | 269 | m_defaultCameraThreadSleepDurationInMs = duration; 270 | } 271 | 272 | void CameraGridFrame::OnSetCameraDefaultResolution(wxCommandEvent&) 273 | { 274 | static const wxSize resolutions[] = 275 | { { 320, 240}, 276 | { 640, 480}, 277 | { 800, 600}, 278 | {1024, 576}, 279 | {1280, 720}, 280 | {1920, 1080}, 281 | {2048, 1080}, 282 | {2560, 1440}, 283 | {3840, 2160} }; 284 | 285 | int resolutionIndex = -1; 286 | wxArrayString resolutionStrings; 287 | 288 | resolutionStrings.push_back(""); 289 | for ( const auto& r : resolutions ) 290 | resolutionStrings.push_back(wxString::Format("%d x %d", r.GetWidth(), r.GetHeight())); 291 | 292 | if ( m_defaultCameraResolution.GetWidth() == 0 ) 293 | { 294 | resolutionIndex = 0; 295 | } 296 | else 297 | { 298 | for ( size_t i = 0; i < WXSIZEOF(resolutions); ++i ) 299 | { 300 | if ( resolutions[i].GetWidth() == m_defaultCameraResolution.GetWidth() 301 | && resolutions[i].GetHeight() == m_defaultCameraResolution.GetHeight() ) 302 | { 303 | resolutionIndex = i + 1; 304 | break; 305 | } 306 | } 307 | } 308 | 309 | resolutionIndex = wxGetSingleChoiceIndex("Width x Height", "Select resolution", resolutionStrings, resolutionIndex, this); 310 | if ( resolutionIndex == -1 ) 311 | return; 312 | 313 | if ( resolutionIndex == 0 ) 314 | m_defaultCameraResolution = wxSize(); 315 | else 316 | m_defaultCameraResolution = resolutions[resolutionIndex-1]; 317 | } 318 | 319 | void CameraGridFrame::OnSetCameraDefaultFPS(wxCommandEvent&) 320 | { 321 | long FPS = wxGetNumberFromUser("Camera FPS (0 = default)", "Number between 0 and 1000", 322 | "Select default camera FPS", 323 | m_defaultCameraFPS, 324 | 0, 1000, this); 325 | 326 | if ( FPS == -1 ) 327 | return; 328 | 329 | m_defaultCameraFPS = FPS; 330 | } 331 | 332 | void CameraGridFrame::OnSetCameraDefaultUseMJPGFourCC(wxCommandEvent& evt) 333 | { 334 | m_defaultUseMJPGFourCC = evt.IsChecked(); 335 | } 336 | 337 | void CameraGridFrame::OnCameraDefaultsReset(wxCommandEvent&) 338 | { 339 | wxMenuBar* menuBar = GetMenuBar(); 340 | 341 | m_defaultCameraBackend = cv::CAP_ANY; 342 | m_defaultCameraThreadSleepDuration = CameraSetupData::SleepFromFPS; 343 | m_defaultCameraThreadSleepDurationInMs = 25; 344 | menuBar->FindItem(ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_FROM_FPS)->Check(); 345 | m_defaultCameraResolution = wxSize(); 346 | m_defaultCameraFPS = 0; 347 | m_defaultUseMJPGFourCC = false; 348 | menuBar->FindItem(ID_CAMERA_SET_DEFAULTS_USE_MJPEG_FOURCC)->Check(false); 349 | } 350 | 351 | // if a camera thumbnail is doubleclicked, show the camera output 352 | // in a full resolution in its own frame 353 | void CameraGridFrame::OnShowOneCameraFrame(wxMouseEvent& evt) 354 | { 355 | evt.Skip(); 356 | 357 | CameraPanel* cameraPanel = dynamic_cast(evt.GetEventObject()); 358 | 359 | wxCHECK_RET(cameraPanel, "in CameraGridFrame::OnShowOneCameraFrame() but event object is not CameraPanel"); 360 | 361 | const wxString cameraName = cameraPanel->GetCameraName(); 362 | 363 | OneCameraFrame* ocFrame = FindOneCameraFrameForCamera(cameraName); 364 | 365 | if ( ocFrame ) 366 | { 367 | ocFrame->Raise(); 368 | return; 369 | } 370 | 371 | if ( cameraPanel->GetStatus() == CameraPanel::Error ) 372 | { 373 | wxLogMessage("Camera '%s' is in error state.", cameraName); 374 | return; 375 | } 376 | 377 | ocFrame = new OneCameraFrame(this, cameraPanel->GetCameraName()); 378 | ocFrame->Show(); 379 | } 380 | 381 | wxString GetCVPropName(cv::VideoCaptureProperties prop) 382 | { 383 | using namespace cv; 384 | 385 | wxString name; 386 | 387 | switch ( prop ) 388 | { 389 | case CAP_PROP_POS_MSEC: name = "POS_MSEC"; break; 390 | case CAP_PROP_POS_FRAMES: name = "POS_FRAMES"; break; 391 | case CAP_PROP_POS_AVI_RATIO: name = "POS_AVI_RATIO"; break; 392 | case CAP_PROP_FRAME_WIDTH: name = "FRAME_WIDTH"; break; 393 | case CAP_PROP_FRAME_HEIGHT: name = "FRAME_HEIGHT"; break; 394 | case CAP_PROP_FPS: name = "FPS"; break; 395 | case CAP_PROP_FOURCC: name = "FOURCC"; break; 396 | case CAP_PROP_FRAME_COUNT: name = "FRAME_COUNT"; break; 397 | case CAP_PROP_FORMAT: name = "FORMAT"; break; 398 | case CAP_PROP_MODE: name = "MODE"; break; 399 | case CAP_PROP_BRIGHTNESS: name = "BRIGHTNESS"; break; 400 | case CAP_PROP_CONTRAST: name = "CONTRAST"; break; 401 | case CAP_PROP_SATURATION: name = "SATURATION"; break; 402 | case CAP_PROP_HUE: name = "HUE"; break; 403 | case CAP_PROP_GAIN: name = "GAIN"; break; 404 | case CAP_PROP_EXPOSURE: name = "EXPOSURE"; break; 405 | case CAP_PROP_CONVERT_RGB: name = "CONVERT_RGB"; break; 406 | case CAP_PROP_WHITE_BALANCE_BLUE_U: name = "WHITE_BALANCE_BLUE_U"; break; 407 | case CAP_PROP_RECTIFICATION: name = "RECTIFICATION"; break; 408 | case CAP_PROP_MONOCHROME: name = "MONOCHROME"; break; 409 | case CAP_PROP_SHARPNESS: name = "SHARPNESS"; break; 410 | case CAP_PROP_AUTO_EXPOSURE: name = "AUTO_EXPOSURE"; break; 411 | case CAP_PROP_GAMMA: name = "GAMMA"; break; 412 | case CAP_PROP_TEMPERATURE: name = "TEMPERATURE"; break; 413 | case CAP_PROP_TRIGGER: name = "TRIGGER"; break; 414 | case CAP_PROP_TRIGGER_DELAY: name = "TRIGGER_DELAY"; break; 415 | case CAP_PROP_WHITE_BALANCE_RED_V: name = "WHITE_BALANCE_RED_V"; break; 416 | case CAP_PROP_ZOOM: name = "ZOOM"; break; 417 | case CAP_PROP_FOCUS: name = "FOCUS"; break; 418 | case CAP_PROP_GUID: name = "GUID"; break; 419 | case CAP_PROP_ISO_SPEED: name = "ISO_SPEED"; break; 420 | case CAP_PROP_BACKLIGHT: name = "BACKLIGHT"; break; 421 | case CAP_PROP_PAN: name = "PAN"; break; 422 | case CAP_PROP_TILT: name = "TILT"; break; 423 | case CAP_PROP_ROLL: name = "ROLL"; break; 424 | case CAP_PROP_IRIS: name = "IRIS"; break; 425 | case CAP_PROP_SETTINGS: name = "SETTINGS"; break; 426 | case CAP_PROP_BUFFERSIZE: name = "BUFFERSIZE"; break; 427 | case CAP_PROP_AUTOFOCUS: name = "AUTOFOCUS"; break; 428 | case CAP_PROP_SAR_NUM: name = "SAR_NUM"; break; 429 | case CAP_PROP_SAR_DEN: name = "SAR_DEN"; break; 430 | case CAP_PROP_BACKEND: name = "BACKEND"; break; 431 | case CAP_PROP_CHANNEL: name = "CHANNEL"; break; 432 | case CAP_PROP_AUTO_WB: name = "AUTO_WB"; break; 433 | case CAP_PROP_WB_TEMPERATURE: name = "WB_TEMPERATURE"; break; 434 | case CAP_PROP_CODEC_PIXEL_FORMAT: name = "CODEC_PIXEL_FORMAT"; break; 435 | #if CHECK_OPENCV_VERSION(4,3,0) 436 | case CAP_PROP_BITRATE: name = "BITRATE"; break; 437 | #endif 438 | #if CHECK_OPENCV_VERSION(4,5,0) 439 | case CAP_PROP_ORIENTATION_META: name = "ORIENTATION_META"; break; 440 | case CAP_PROP_ORIENTATION_AUTO: name = "ORIENTATION_AUTO"; break; 441 | #endif 442 | #if CHECK_OPENCV_VERSION(4,5,2) 443 | case CAP_PROP_HW_ACCELERATION: name = "HW_ACCELERATION"; break; 444 | case CAP_PROP_HW_DEVICE: name = "HW_DEVICE"; break; 445 | #endif 446 | #if CHECK_OPENCV_VERSION(4,5,3) 447 | case CAP_PROP_HW_ACCELERATION_USE_OPENCL: name = "HW_ACCELERATION_USE_OPENCL"; break; 448 | #endif 449 | #if CHECK_OPENCV_VERSION(4,5,4) 450 | case CAP_PROP_OPEN_TIMEOUT_MSEC: name = "OPEN_TIMEOUT_MSEC"; break; 451 | case CAP_PROP_READ_TIMEOUT_MSEC: name = "READ_TIMEOUT_MSEC"; break; 452 | case CAP_PROP_STREAM_OPEN_TIME_USEC: name = "STREAM_OPEN_TIME_USEC"; break; 453 | #endif 454 | #if CHECK_OPENCV_VERSION(4,5,5) 455 | case CAP_PROP_VIDEO_TOTAL_CHANNELS: name = "VIDEO_TOTAL_CHANNELS"; break; 456 | case CAP_PROP_VIDEO_STREAM: name = "VIDEO_STREAM"; break; 457 | case CAP_PROP_AUDIO_STREAM: name = "AUDIO_STREAM"; break; 458 | case CAP_PROP_AUDIO_POS: name = "AUDIO_POS"; break; 459 | case CAP_PROP_AUDIO_SHIFT_NSEC: name = "AUDIO_SHIFT_NSEC"; break; 460 | case CAP_PROP_AUDIO_DATA_DEPTH: name = "AUDIO_DATA_DEPTH"; break; 461 | case CAP_PROP_AUDIO_SAMPLES_PER_SECOND: name = "AUDIO_SAMPLES_PER_SECOND"; break; 462 | case CAP_PROP_AUDIO_BASE_INDEX: name = "AUDIO_BASE_INDEX"; break; 463 | case CAP_PROP_AUDIO_TOTAL_CHANNELS: name = "AUDIO_TOTAL_CHANNELS"; break; 464 | case CAP_PROP_AUDIO_TOTAL_STREAMS: name = "AUDIO_TOTAL_STREAMS"; break; 465 | case CAP_PROP_AUDIO_SYNCHRONIZE: name = "AUDIO_SYNCHRONIZE"; break; 466 | case CAP_PROP_LRF_HAS_KEY_FRAME: name = "LRF_HAS_KEY_FRAME"; break; 467 | case CAP_PROP_CODEC_EXTRADATA_INDEX: name = "CODEC_EXTRADATA_INDEX"; break; 468 | #endif 469 | } 470 | 471 | return name; 472 | } 473 | 474 | void CameraGridFrame::OnCameraContextMenu(wxContextMenuEvent& evt) 475 | { 476 | CameraPanel* cameraPanel = dynamic_cast(evt.GetEventObject()); 477 | 478 | if ( !cameraPanel ) 479 | cameraPanel = dynamic_cast(wxFindWindowAtPoint(wxGetMousePosition())); 480 | 481 | wxCHECK_RET(cameraPanel, "in CameraGridFrame::OnCameraContextMenu() but could not determine the thumbnail panel"); 482 | 483 | auto it = m_cameras.find(cameraPanel->GetCameraName()); 484 | 485 | if ( it == m_cameras.end() || !it->second.thread->IsCapturing() ) 486 | return; 487 | 488 | 489 | wxMenu menu; 490 | int id = wxID_NONE; 491 | 492 | menu.Append(ID_CAMERA_GET_INFO, "Get Camera Information"); 493 | menu.Append(ID_CAMERA_SET_THREAD_SLEEP_DURATION, "Set Thread Sleep duration..."); 494 | menu.Append(ID_CAMERA_GET_VCPROP, "Get VideoCapture Property..."); 495 | menu.Append(ID_CAMERA_SET_VCPROP, "Set VideoCapture Property..."); 496 | 497 | id = cameraPanel->GetPopupMenuSelectionFromUser(menu); 498 | if ( id == wxID_NONE ) 499 | return; 500 | 501 | CameraCommandData commandData; 502 | 503 | if ( id == ID_CAMERA_GET_INFO ) 504 | { 505 | commandData.command = CameraCommandData::GetCameraInfo; 506 | it->second.commandDatas->Post(commandData); 507 | } 508 | else if ( id == ID_CAMERA_SET_THREAD_SLEEP_DURATION ) 509 | { 510 | long duration = wxGetNumberFromUser("Sleep duration in ms", "Number between 0 (no sleep) and 1000", 511 | "Select default CameraThread sleep duration", 512 | m_defaultCameraThreadSleepDurationInMs, 513 | 0, 1000, this); 514 | 515 | if ( duration == -1 ) 516 | return; 517 | 518 | commandData.command = CameraCommandData::SetThreadSleepDuration; 519 | commandData.parameter = duration; 520 | it->second.commandDatas->Post(commandData); 521 | } 522 | else if ( id == ID_CAMERA_GET_VCPROP ) 523 | { 524 | const int prop = SelectCaptureProperty("Property to Get"); 525 | 526 | if ( prop == -1 ) 527 | return; 528 | 529 | CameraCommandData::VCPropCommandParameter param; 530 | CameraCommandData::VCPropCommandParameters params; 531 | 532 | commandData.command = CameraCommandData::GetVCProp; 533 | 534 | param.id = prop; 535 | params.push_back(param); 536 | commandData.parameter = params; 537 | 538 | it->second.commandDatas->Post(commandData); 539 | } 540 | else if ( id == ID_CAMERA_SET_VCPROP ) 541 | { 542 | const int prop = SelectCaptureProperty("Property to Set"); 543 | 544 | if (prop == -1) 545 | return; 546 | 547 | // wxWidgets does not have a convenience function asking the user for a double 548 | wxString number = wxGetTextFromUser("Enter property value as double in C locale", "Value", "", this); 549 | 550 | if ( number.empty() ) 551 | return; 552 | 553 | double value; 554 | 555 | if ( !number.ToCDouble(&value) ) 556 | { 557 | wxLogError("Invalid property value."); 558 | return; 559 | } 560 | 561 | CameraCommandData::VCPropCommandParameter param; 562 | CameraCommandData::VCPropCommandParameters params; 563 | 564 | commandData.command = CameraCommandData::SetVCProp; 565 | 566 | param.id = prop; 567 | param.value = value; 568 | params.push_back(param); 569 | commandData.parameter = params; 570 | 571 | it->second.commandDatas->Post(commandData); 572 | } 573 | else 574 | { 575 | wxFAIL_MSG("Invalid command"); 576 | } 577 | } 578 | 579 | void CameraGridFrame::OnUpdateInfo(wxTimerEvent&) 580 | { 581 | static wxULongLong prevFramesProcessed{0}; 582 | 583 | size_t camerasCapturing{0}; 584 | 585 | for ( const auto& c : m_cameras ) 586 | { 587 | if ( c.second.thread->IsCapturing() ) 588 | camerasCapturing++; 589 | } 590 | 591 | SetStatusText(wxString::Format("%zu cameras (%zu capturing)", 592 | m_cameras.size(), camerasCapturing), 0); 593 | 594 | // This number is not indicative of the maximum possible performance. 595 | // It depends on how many cameras are there, on their fps and time to sleep in the thread 596 | // and last but not least on the interval and resolution of m_processNewCameraFrameDataTimer. 597 | SetStatusText(wxString::Format("%s frames processed by GUI in the last second", 598 | (m_framesProcessed - prevFramesProcessed).ToString()), 1); 599 | 600 | prevFramesProcessed = m_framesProcessed; 601 | } 602 | 603 | void CameraGridFrame::AddCamera(const wxString& address) 604 | { 605 | const wxSize thumbnailSize = wxSize(320, 180); 606 | 607 | static int newCameraId = 0; 608 | 609 | CameraView cameraView; 610 | wxString cameraName = wxString::Format("CAM #%d", newCameraId++); 611 | CameraSetupData cameraInitData; 612 | 613 | cameraInitData.name = cameraName; 614 | cameraInitData.address = address; 615 | cameraInitData.apiPreference = m_defaultCameraBackend; 616 | cameraInitData.sleepDuration = m_defaultCameraThreadSleepDuration; 617 | cameraInitData.frameSize = m_defaultCameraResolution; 618 | cameraInitData.FPS = m_defaultCameraFPS; 619 | cameraInitData.useMJPGFourCC = m_defaultUseMJPGFourCC; 620 | 621 | cameraInitData.eventSink = this; 622 | cameraInitData.frames = &m_newCameraFrameData; 623 | cameraInitData.framesCS = &m_newCameraFrameDataCS; 624 | cameraInitData.thumbnailSize = thumbnailSize; 625 | 626 | cameraInitData.commands = new CameraCommandDatas; 627 | 628 | cameraView.thread = new CameraThread(cameraInitData); 629 | 630 | cameraView.thumbnailPanel = new CameraPanel(this, cameraName); 631 | cameraView.thumbnailPanel->SetMinSize(thumbnailSize); 632 | cameraView.thumbnailPanel->SetMaxSize(thumbnailSize); 633 | cameraView.thumbnailPanel->Bind(wxEVT_LEFT_DCLICK, &CameraGridFrame::OnShowOneCameraFrame, this); 634 | cameraView.thumbnailPanel->Bind(wxEVT_CONTEXT_MENU, &CameraGridFrame::OnCameraContextMenu, this); 635 | GetSizer()->Add(cameraView.thumbnailPanel, wxSizerFlags().Border()); 636 | Layout(); 637 | 638 | cameraView.commandDatas = cameraInitData.commands; 639 | 640 | m_cameras[cameraName] = cameraView; 641 | 642 | if ( cameraView.thread->Run() != wxTHREAD_NO_ERROR ) 643 | wxLogError("Could not create the worker thread needed to retrieve the images from camera '%s'.", cameraName); 644 | } 645 | 646 | void CameraGridFrame::RemoveCamera(const wxString& cameraName) 647 | { 648 | auto it = m_cameras.find(cameraName); 649 | 650 | wxCHECK_RET(it != m_cameras.end(), wxString::Format("Camera '%s' not found, could not be deleted.", cameraName)); 651 | 652 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Removing camera '%s'...", cameraName); 653 | it->second.thread->Delete(nullptr, wxTHREAD_WAIT_BLOCK); 654 | delete it->second.thread; 655 | 656 | GetSizer()->Detach(it->second.thumbnailPanel); 657 | it->second.thumbnailPanel->Destroy(); 658 | 659 | OneCameraFrame* ocFrame = FindOneCameraFrameForCamera(cameraName); 660 | 661 | if ( ocFrame ) 662 | { 663 | ocFrame->Destroy(); 664 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Closed OneCameraFrame for camera '%s'.", cameraName); 665 | } 666 | 667 | delete it->second.commandDatas; 668 | 669 | m_cameras.erase(it); 670 | Layout(); 671 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Removed camera '%s'.", cameraName); 672 | } 673 | 674 | void CameraGridFrame::RemoveAllCameras() 675 | { 676 | wxWindowUpdateLocker locker; 677 | 678 | while ( !m_cameras.empty() ) 679 | RemoveCamera(wxString(m_cameras.begin()->first)); 680 | 681 | m_cameras.clear(); 682 | Layout(); 683 | } 684 | 685 | void CameraGridFrame::OnProcessNewCameraFrameData(wxTimerEvent&) 686 | { 687 | CameraFrameDataPtrs frameData; 688 | wxStopWatch stopWatch; 689 | 690 | stopWatch.Start(); 691 | { 692 | wxCriticalSectionLocker locker(m_newCameraFrameDataCS); 693 | 694 | if ( m_newCameraFrameData.empty() ) 695 | return; 696 | 697 | frameData = std::move(m_newCameraFrameData); 698 | } 699 | 700 | for ( const auto& fd : frameData ) 701 | { 702 | const wxString cameraName = fd->GetCameraName(); 703 | auto it = m_cameras.find(cameraName); 704 | 705 | if ( it == m_cameras.end() || !it->second.thread->IsCapturing() ) 706 | continue; // ignore yet-unprocessed frames from removed or errored cameras 707 | 708 | CameraPanel* cameraThumbnailPanel = FindThumbnailPanelForCamera(cameraName); 709 | const wxBitmap* cameraFrame = fd->GetFrame(); 710 | const wxBitmap* cameraFrameThumbnail = fd->GetThumbnail(); 711 | // capturedToProcessTime obviously depends on timer interval and resolution 712 | const wxLongLong capturedToProcessTime = wxGetUTCTimeMillis() - fd->GetCapturedTime(); 713 | 714 | if ( !cameraFrame || !cameraFrame->IsOk() ) 715 | { 716 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Frame with a null or invalid frame (camera '%s', frame #%s)!", 717 | cameraName, fd->GetFrameNumber().ToString()); 718 | continue; 719 | } 720 | 721 | if ( cameraThumbnailPanel ) 722 | { 723 | if ( cameraFrameThumbnail && cameraFrameThumbnail->IsOk() ) 724 | cameraThumbnailPanel->SetBitmap(*cameraFrameThumbnail); 725 | else 726 | cameraThumbnailPanel->SetBitmap(wxBitmap(), CameraPanel::Error); 727 | } 728 | 729 | OneCameraFrame* ocFrame = FindOneCameraFrameForCamera(cameraName); 730 | 731 | if ( ocFrame ) 732 | ocFrame->SetCameraBitmap(*cameraFrame); 733 | 734 | m_framesProcessed++; 735 | 736 | #if 0 737 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Frame from camera '%s' for frame #%s with resolution %dx%d took %ld ms from capture to process" 738 | " (OpenCV times: retrieve %ld ms, convert %ld ms, thumbnail %s ms).", 739 | cameraName, 740 | fd->GetFrameNumber().ToString(), 741 | cameraFrame->GetWidth(), cameraFrame->GetHeight(), 742 | capturedToProcessTime.ToLong(), 743 | fd->GetTimeToRetrieve(), fd->GetTimeToConvert(), 744 | cameraFrameThumbnail ? wxString::Format("%ld", fd->GetTimeToCreateThumbnail()) : "n/a"); 745 | #endif 746 | } 747 | 748 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Processed %zu new camera frames in %ld ms.", frameData.size(), stopWatch.Time()); 749 | 750 | frameData.clear(); 751 | } 752 | 753 | void CameraGridFrame::OnCameraCaptureStarted(CameraEvent& evt) 754 | { 755 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Started capturing from camera '%s' (fps: %s, backend: %s)'.", 756 | evt.GetCameraName(), 757 | evt.GetInt() ? wxString::Format("%d", evt.GetInt()) : "n/a", 758 | evt.GetString()); 759 | } 760 | 761 | void CameraGridFrame::OnCameraCommandResult(CameraEvent& evt) 762 | { 763 | const CameraCommandData commandData = evt.GetCommandResult(); 764 | 765 | wxString infoMessage; 766 | 767 | if ( commandData.command == CameraCommandData::GetCameraInfo ) 768 | { 769 | CameraCommandData::CameraInfo cameraInfo; 770 | wxString s; 771 | 772 | commandData.parameter.GetAs(&cameraInfo); 773 | 774 | infoMessage.Printf("CameraInfo for camera '%s':\n", evt.GetCameraName()); 775 | 776 | switch ( cameraInfo.threadSleepDuration ) 777 | { 778 | case CameraSetupData::SleepFromFPS: 779 | s = "FPS-based"; 780 | break; 781 | case CameraSetupData::SleepNone: 782 | s = "None"; 783 | break; 784 | default: 785 | s.Printf("%ld", cameraInfo.threadSleepDuration); 786 | } 787 | 788 | infoMessage += " Thread sleep duration: " + s + "\n"; 789 | infoMessage += " Frames captured: " + cameraInfo.framesCapturedCount.ToString() + "\n";; 790 | infoMessage += " Backend name: " + cameraInfo.cameraCaptureBackendName + "\n"; 791 | infoMessage += " Address: " + cameraInfo.cameraAddress + "\n"; 792 | } 793 | else if ( commandData.command == CameraCommandData::SetThreadSleepDuration ) 794 | { 795 | long duration; 796 | 797 | commandData.parameter.GetAs(&duration); 798 | 799 | infoMessage.Printf("Thread sleep duration for camera '%s' was set to %ld:\n.", evt.GetCameraName(), duration); 800 | } 801 | else if ( commandData.command == CameraCommandData::GetVCProp ) 802 | { 803 | CameraCommandData::VCPropCommandParameters params; 804 | wxString s; 805 | 806 | commandData.parameter.GetAs(¶ms); 807 | 808 | infoMessage.Printf("Retrieved capture properties from camera '%s':\n", evt.GetCameraName()); 809 | 810 | for ( const auto& p : params ) 811 | { 812 | if ( p.id == cv::CAP_PROP_FOURCC && p.value != 0. ) 813 | { 814 | const int fourCCInt = static_cast(p.value); 815 | const char fourCCStr[] = {(char)(fourCCInt & 0XFF), 816 | (char)((fourCCInt & 0XFF00) >> 8), 817 | (char)((fourCCInt & 0XFF0000) >> 16), 818 | (char)((fourCCInt & 0XFF000000) >> 24), 0}; 819 | 820 | s = fourCCStr; 821 | } 822 | else 823 | { 824 | s.Printf("%G", p.value); 825 | } 826 | infoMessage += wxString::Format(" %s: %s\n", 827 | GetCVPropName(static_cast(p.id)), s); 828 | } 829 | 830 | } 831 | else if ( commandData.command == CameraCommandData::SetVCProp ) 832 | { 833 | CameraCommandData::VCPropCommandParameters params; 834 | 835 | commandData.parameter.GetAs(¶ms); 836 | 837 | infoMessage.Printf("Set capture properties for camera '%s':\n", evt.GetCameraName()); 838 | 839 | for ( const auto& p : params ) 840 | { 841 | infoMessage += wxString::Format(" %s to %G: %s\n", 842 | GetCVPropName(static_cast(p.id)), p.value, 843 | p.succeeded ? "Succeeded" : "FAILED"); 844 | } 845 | } 846 | else 847 | { 848 | wxFAIL_MSG("Invalid command"); 849 | } 850 | 851 | wxLogMessage(infoMessage); 852 | } 853 | 854 | void CameraGridFrame::OnCameraErrorOpen(CameraEvent& evt) 855 | { 856 | const wxString cameraName = evt.GetCameraName(); 857 | 858 | ShowErrorForCamera(cameraName, wxString::Format("Could not open camera '%s'.", cameraName)); 859 | } 860 | 861 | void CameraGridFrame::OnCameraErrorEmpty(CameraEvent& evt) 862 | { 863 | const wxString cameraName = evt.GetCameraName(); 864 | 865 | ShowErrorForCamera(cameraName, wxString::Format("Connection to camera '%s' lost.", cameraName)); 866 | } 867 | 868 | void CameraGridFrame::OnCameraErrorException(CameraEvent& evt) 869 | { 870 | const wxString cameraName = evt.GetCameraName(); 871 | 872 | ShowErrorForCamera(cameraName, wxString::Format("Exception in camera '%s': %s", cameraName, evt.GetString())); 873 | } 874 | 875 | void CameraGridFrame::ShowErrorForCamera(const wxString& cameraName, const wxString& message) 876 | { 877 | CameraPanel* cameraPanel = FindThumbnailPanelForCamera(cameraName); 878 | 879 | if ( cameraPanel ) 880 | cameraPanel->SetBitmap(wxBitmap(), CameraPanel::Error); 881 | 882 | OneCameraFrame* ocFrame = FindOneCameraFrameForCamera(cameraName); 883 | 884 | if ( ocFrame ) 885 | ocFrame->SetCameraBitmap(wxBitmap(), CameraPanel::Error); 886 | 887 | wxLogError(message); 888 | } 889 | 890 | CameraPanel* CameraGridFrame::FindThumbnailPanelForCamera(const wxString& cameraName) const 891 | { 892 | auto it = m_cameras.find(cameraName); 893 | 894 | if ( it == m_cameras.end() ) 895 | return nullptr; 896 | 897 | return it->second.thumbnailPanel; 898 | } 899 | 900 | OneCameraFrame* CameraGridFrame::FindOneCameraFrameForCamera(const wxString& cameraName) const 901 | { 902 | const auto& children = GetChildren(); 903 | 904 | for ( const auto child : children ) 905 | { 906 | OneCameraFrame* ocFrame = dynamic_cast(child); 907 | 908 | if ( !ocFrame ) 909 | continue; 910 | 911 | if ( ocFrame->GetCameraName().IsSameAs(cameraName) ) 912 | return ocFrame; 913 | } 914 | 915 | return nullptr; 916 | } 917 | 918 | int CameraGridFrame::SelectCaptureProperty(const wxString& message) 919 | { 920 | wxArrayString properties; 921 | 922 | for ( int prop = cv::CAP_PROP_POS_MSEC; prop < cv::CV__CAP_PROP_LATEST; ++prop ) 923 | { 924 | const wxString propName = GetCVPropName(static_cast(prop)); 925 | 926 | if ( !propName.empty() ) 927 | properties.push_back(propName); 928 | } 929 | 930 | int result = wxGetSingleChoiceIndex(message, "Select OpenCV Capture Property", properties, 0, this); 931 | 932 | if ( result > cv::CAP_PROP_ISO_SPEED ) 933 | { 934 | // hack needed due to missing cv::CAP_PROP_ with value 31 between CAP_PROP_ISO_SPEED and CAP_PROP_BACKLIGHT 935 | result++; 936 | } 937 | 938 | return result; 939 | } -------------------------------------------------------------------------------- /cameragridframe.h: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: cameragridframe.cpp 3 | // Purpose: Displays thumbnails from multiple cameras 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | #ifndef CAMERAGRIDFRAME_H 11 | #define CAMERAGRIDFRAME_H 12 | 13 | #include 14 | 15 | #include 16 | 17 | #include "camerathread.h" 18 | 19 | // forward declarations 20 | class CameraPanel; 21 | class OneCameraFrame; 22 | 23 | class CameraGridFrame : public wxFrame 24 | { 25 | public: 26 | CameraGridFrame(wxWindow* parent = nullptr); 27 | ~CameraGridFrame(); 28 | private: 29 | enum 30 | { 31 | ID_CAMERA_ADD_CUSTOM = wxID_HIGHEST + 1, 32 | ID_CAMERA_ADD_DEFAULT_WEBCAM, 33 | 34 | ID_CAMERA_ADD_ALL_IP_ABOVE, 35 | 36 | ID_CAMERA_ADD_DOLBYVISIONHLS, 37 | 38 | ID_CAMERA_REMOVE, 39 | ID_CAMERA_REMOVE_ALL, 40 | 41 | ID_CAMERA_SET_DEFAULTS_BACKEND, 42 | ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_FROM_FPS, 43 | ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_NONE, 44 | ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_CUSTOM, 45 | ID_CAMERA_SET_DEFAULTS_THREAD_SLEEP_CUSTOM_SET, 46 | ID_CAMERA_SET_DEFAULTS_RESOLUTION, 47 | ID_CAMERA_SET_DEFAULTS_FPS, 48 | ID_CAMERA_SET_DEFAULTS_USE_MJPEG_FOURCC, 49 | ID_CAMERA_SET_DEFAULTS_RESET, 50 | 51 | ID_CAMERA_GET_INFO, 52 | ID_CAMERA_SET_THREAD_SLEEP_DURATION, 53 | ID_CAMERA_GET_VCPROP, 54 | ID_CAMERA_SET_VCPROP, 55 | }; 56 | 57 | struct CameraView 58 | { 59 | CameraThread* thread{nullptr}; 60 | CameraPanel* thumbnailPanel{nullptr}; 61 | CameraCommandDatas* commandDatas{nullptr}; 62 | }; 63 | 64 | // default timer interval in ms for processing new camera frame data from worker threads 65 | // see m_processNewCameraFrameDataInterval 66 | static const long ms_defaultProcessNewCameraFrameDataInterval = 30; 67 | 68 | // camera name to CameraView 69 | std::map m_cameras; 70 | long m_processNewCameraFrameDataInterval{ms_defaultProcessNewCameraFrameDataInterval}; 71 | wxTimer m_processNewCameraFrameDataTimer; 72 | CameraFrameDataPtrs m_newCameraFrameData; 73 | wxCriticalSection m_newCameraFrameDataCS; 74 | 75 | long m_defaultCameraBackend{0}; 76 | long m_defaultCameraThreadSleepDuration{CameraSetupData::SleepFromFPS}; 77 | long m_defaultCameraThreadSleepDurationInMs{25}; // used for custom sleep duration 78 | wxSize m_defaultCameraResolution; 79 | int m_defaultCameraFPS{0}; 80 | bool m_defaultUseMJPGFourCC{false}; 81 | 82 | wxTimer m_updateInfoTimer; 83 | wxULongLong m_framesProcessed{0}; 84 | 85 | void OnAddCamera(wxCommandEvent&); 86 | void OnAddAllIPCamerasAbove(wxCommandEvent&); 87 | void OnRemoveCamera(wxCommandEvent&); 88 | void OnRemoveAllCameras(wxCommandEvent&); 89 | 90 | void OnSetCameraDefaultBackend(wxCommandEvent&); 91 | void OnSetCameraDefaultThreadSleepFromFPS(wxCommandEvent&); 92 | void OnSetCameraDefaultThreadSleepNone(wxCommandEvent&); 93 | void OnSetCameraDefaultThreadSleepCustom(wxCommandEvent&); 94 | void OnSetCameraDefaultThreadSleepCustomSetDuration(wxCommandEvent&); 95 | void OnSetCameraDefaultResolution(wxCommandEvent&); 96 | void OnSetCameraDefaultFPS(wxCommandEvent&); 97 | void OnSetCameraDefaultUseMJPGFourCC(wxCommandEvent& evt); 98 | void OnCameraDefaultsReset(wxCommandEvent&); 99 | 100 | void OnShowOneCameraFrame(wxMouseEvent& evt); 101 | 102 | void OnCameraContextMenu(wxContextMenuEvent& evt); 103 | 104 | void OnUpdateInfo(wxTimerEvent&); 105 | 106 | void AddCamera(const wxString& address); 107 | void RemoveCamera(const wxString& cameraName); 108 | void RemoveAllCameras(); 109 | 110 | void OnProcessNewCameraFrameData(wxTimerEvent&); 111 | 112 | void OnCameraCaptureStarted(CameraEvent& evt); 113 | 114 | void OnCameraCommandResult(CameraEvent& evt); 115 | 116 | void OnCameraErrorOpen(CameraEvent& evt); 117 | void OnCameraErrorEmpty(CameraEvent& evt); 118 | void OnCameraErrorException(CameraEvent& evt); 119 | 120 | void ShowErrorForCamera(const wxString& cameraName, const wxString& message); 121 | 122 | CameraPanel* FindThumbnailPanelForCamera(const wxString& cameraName) const; 123 | OneCameraFrame* FindOneCameraFrameForCamera(const wxString& cameraName) const; 124 | 125 | int SelectCaptureProperty(const wxString& message); 126 | }; 127 | 128 | #endif // #ifndef CAMERAGRIDFRAME_H -------------------------------------------------------------------------------- /camerapanel.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: camerapanel.cpp 3 | // Purpose: Displays a bitmap and camera status 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | 11 | #include 12 | #include 13 | 14 | #include "camerapanel.h" 15 | 16 | CameraPanel::CameraPanel(wxWindow* parent, const wxString& cameraName, 17 | bool drawPaintTime, Status status) 18 | : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE | wxBORDER_RAISED), 19 | m_cameraName(cameraName), m_drawPaintTime(drawPaintTime), m_status(status) 20 | { 21 | SetBackgroundStyle(wxBG_STYLE_PAINT); 22 | Bind(wxEVT_PAINT, &CameraPanel::OnPaint, this); 23 | } 24 | 25 | void CameraPanel::SetBitmap(const wxBitmap& bitmap, Status status) 26 | { 27 | m_bitmap = bitmap; 28 | m_status = status; 29 | 30 | Refresh(); Update(); 31 | } 32 | 33 | // On MSW, displaying 4k bitmaps from 60 fps camera with 34 | // wx(Auto)BufferedPaintDC in some scenarios meant the application 35 | // after while started for some reason lagging very badly, 36 | // even unable to process the camera frames. This did not happen 37 | // when using wxPaintDC instead. However, drawing the same way with 38 | // wxPaintDC instead of wx(Auto)BufferedPaintDC meant the panels were flashing. 39 | // In the end, the old-fashioned way with wxMemoryDC 40 | // is used, even if it means that drawing cannot be timed (easily). 41 | 42 | void CameraPanel::OnPaint(wxPaintEvent&) 43 | { 44 | wxDC* paintDC{nullptr}; 45 | 46 | #if CAMERAPANEL_USE_AUTOBUFFEREDPAINTDC 47 | wxAutoBufferedPaintDC dc(this); 48 | wxStopWatch stopWatch; 49 | 50 | paintDC = &dc; 51 | stopWatch.Start(); 52 | #else 53 | const wxSize clientSize(GetClientSize()); 54 | 55 | wxPaintDC dc(this); 56 | 57 | if ( clientSize.GetWidth() < 1 || clientSize.GetHeight() < 1 ) 58 | return; 59 | 60 | wxBitmap memDCBitmap(clientSize); 61 | wxMemoryDC memDC(&dc); 62 | 63 | memDC.SelectObject(memDCBitmap); 64 | paintDC = &memDC; 65 | #endif 66 | 67 | wxString statusString; 68 | wxColour statusColor(*wxBLUE); 69 | 70 | paintDC->SetBackground(*wxBLACK_BRUSH); 71 | paintDC->Clear(); 72 | 73 | if ( m_bitmap.IsOk() ) 74 | paintDC->DrawBitmap(m_bitmap, 0, 0, false); 75 | 76 | switch ( m_status ) 77 | { 78 | case Connecting: 79 | statusString = "Connecting"; 80 | statusColor = *wxBLUE; 81 | break; 82 | case Receiving: 83 | statusString = "Receiving"; 84 | statusColor = *wxGREEN; 85 | break; 86 | case Error: 87 | statusString = "ERROR"; 88 | statusColor = *wxRED; 89 | break; 90 | } 91 | 92 | wxDCTextColourChanger tcChanger(*paintDC, statusColor); 93 | wxString infoText(wxString::Format("%s: %s", m_cameraName, statusString)); 94 | 95 | #if CAMERAPANEL_USE_AUTOBUFFEREDPAINTDC 96 | if ( m_drawPaintTime && m_status == Receiving ) 97 | infoText.Printf("%s\nFrame painted in %ld ms", infoText, stopWatch.Time()); 98 | #endif 99 | 100 | paintDC->DrawText(infoText, 5, 5); 101 | 102 | #if !CAMERAPANEL_USE_AUTOBUFFEREDPAINTDC 103 | dc.Blit(wxPoint(0, 0), clientSize, &memDC, wxPoint(0, 0)); 104 | #endif 105 | } -------------------------------------------------------------------------------- /camerapanel.h: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: camerapanel.h 3 | // Purpose: Displays a bitmap and camera status 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | 11 | #ifndef CAMERAPANEL_H 12 | #define CAMERAPANEL_H 13 | 14 | #include 15 | 16 | class CameraPanel : public wxPanel 17 | { 18 | public: 19 | enum Status { Connecting, Receiving, Error }; 20 | 21 | CameraPanel(wxWindow* parent, const wxString& cameraName, 22 | bool drawPaintTime = false, Status status = Connecting); 23 | 24 | void SetBitmap(const wxBitmap& bitmap, Status status = Receiving); 25 | 26 | wxString GetCameraName() const { return m_cameraName; } 27 | Status GetStatus() const { return m_status; } 28 | private: 29 | wxBitmap m_bitmap; 30 | wxString m_cameraName; 31 | bool m_drawPaintTime; 32 | Status m_status{Connecting}; 33 | 34 | void OnPaint(wxPaintEvent&); 35 | }; 36 | 37 | #endif // #ifndef CAMERAPANEL_H -------------------------------------------------------------------------------- /camerathread.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: camerathread.cpp 3 | // Purpose: Thread and event for retrieving images from a camera with cv::VideoCapture 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include "camerathread.h" 15 | #include "convertmattowxbmp.h" 16 | 17 | 18 | /*********************************************************************************************** 19 | 20 | CameraEvent 21 | 22 | ***********************************************************************************************/ 23 | 24 | // see the header for description 25 | wxDEFINE_EVENT(EVT_CAMERA_CAPTURE_STARTED, CameraEvent); 26 | wxDEFINE_EVENT(EVT_CAMERA_COMMAND_RESULT, CameraEvent); 27 | wxDEFINE_EVENT(EVT_CAMERA_ERROR_OPEN, CameraEvent); 28 | wxDEFINE_EVENT(EVT_CAMERA_ERROR_EMPTY, CameraEvent); 29 | wxDEFINE_EVENT(EVT_CAMERA_ERROR_EXCEPTION, CameraEvent); 30 | 31 | 32 | /*********************************************************************************************** 33 | 34 | CameraFrameData 35 | 36 | ***********************************************************************************************/ 37 | 38 | CameraFrameData::CameraFrameData(const wxString& cameraName, 39 | const wxULongLong frameNumber) 40 | : m_cameraName(cameraName), m_frameNumber(frameNumber) 41 | {} 42 | 43 | CameraFrameData::~CameraFrameData() 44 | { 45 | if ( m_frame ) 46 | delete m_frame; 47 | if ( m_thumbnail ) 48 | delete m_thumbnail; 49 | } 50 | 51 | /*********************************************************************************************** 52 | 53 | CameraInitData 54 | 55 | ***********************************************************************************************/ 56 | 57 | 58 | bool CameraSetupData::IsOk() const 59 | { 60 | return !name.empty() 61 | && !address.empty() 62 | && defaultFPS > 0 63 | && eventSink 64 | && frames && framesCS 65 | && frameSize.GetWidth() >= 0 && frameSize.GetHeight() >= 0 66 | && commands; 67 | } 68 | 69 | /*********************************************************************************************** 70 | 71 | CameraThread 72 | 73 | ***********************************************************************************************/ 74 | 75 | CameraThread::CameraThread(const CameraSetupData& cameraSetupData) 76 | : wxThread(wxTHREAD_JOINABLE), 77 | m_cameraSetupData(cameraSetupData) 78 | { 79 | wxCHECK_RET(m_cameraSetupData.IsOk(), "Invalid camera initialization data"); 80 | } 81 | 82 | wxThread::ExitCode CameraThread::Entry() 83 | { 84 | #if wxCHECK_VERSION(3, 1, 6) 85 | SetName(wxString::Format("CameraThread %s", GetCameraName())); 86 | #endif 87 | 88 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Entered CameraThread for camera '%s', attempting to start capture...", GetCameraName()); 89 | 90 | if ( !InitCapture() ) 91 | { 92 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Failed to start capture for camera '%s'", GetCameraName()); 93 | m_cameraSetupData.eventSink->QueueEvent(new CameraEvent(EVT_CAMERA_ERROR_OPEN, GetCameraName())); 94 | return static_cast(nullptr); 95 | } 96 | 97 | const bool createThumbnail = m_cameraSetupData.thumbnailSize.GetWidth() > 0 && m_cameraSetupData.thumbnailSize.GetHeight() > 0; 98 | 99 | CameraEvent* evt{nullptr}; 100 | cv::Mat matFrame; 101 | wxStopWatch stopWatch; 102 | long msPerFrame; 103 | 104 | m_captureStartedTime = wxGetUTCTimeMillis(); 105 | m_isCapturing = true; 106 | 107 | if ( m_cameraSetupData.frameSize.GetWidth() > 0 ) 108 | SetCameraResolution(m_cameraSetupData.frameSize); 109 | if ( m_cameraSetupData.useMJPGFourCC ) 110 | SetCameraUseMJPEG(); 111 | if ( m_cameraSetupData.FPS > 0 ) 112 | SetCameraFPS(m_cameraSetupData.FPS); 113 | 114 | m_cameraSetupData.FPS = m_cameraCapture->get(static_cast(cv::CAP_PROP_FPS)); 115 | 116 | evt = new CameraEvent(EVT_CAMERA_CAPTURE_STARTED, GetCameraName()); 117 | evt->SetString(wxString(m_cameraCapture->getBackendName())); 118 | evt->SetInt(m_cameraSetupData.FPS); 119 | m_cameraSetupData.eventSink->QueueEvent(evt); 120 | 121 | 122 | while ( !TestDestroy() ) 123 | { 124 | try 125 | { 126 | CameraFrameDataPtr frameData(new CameraFrameData(GetCameraName(), m_framesCapturedCount++)); 127 | wxLongLong frameCaptureStartedTime; 128 | CameraCommandData commandData; 129 | 130 | frameCaptureStartedTime = wxGetUTCTimeMillis(); 131 | 132 | if ( m_cameraSetupData.commands->ReceiveTimeout(0, commandData) == wxMSGQUEUE_NO_ERROR ) 133 | ProcessCameraCommand(commandData); 134 | 135 | if ( m_cameraSetupData.FPS > 0 ) 136 | msPerFrame = 1000 / m_cameraSetupData.FPS; 137 | else 138 | msPerFrame = 1000 / m_cameraSetupData.defaultFPS; 139 | 140 | stopWatch.Start(); 141 | (*m_cameraCapture) >> matFrame; 142 | frameData->SetTimeToRetrieve(stopWatch.Time()); 143 | frameData->SetCapturedTime(wxGetUTCTimeMillis()); 144 | 145 | if ( !matFrame.empty() ) 146 | { 147 | stopWatch.Start(); 148 | frameData->SetFrame(new wxBitmap(matFrame.cols, matFrame.rows, 24)); 149 | ConvertMatBitmapTowxBitmap(matFrame, *frameData->GetFrame()); 150 | frameData->SetTimeToConvert(stopWatch.Time()); 151 | 152 | if ( createThumbnail ) 153 | { 154 | cv::Mat matThumbnail; 155 | 156 | stopWatch.Start(); 157 | cv::resize(matFrame, matThumbnail, cv::Size(m_cameraSetupData.thumbnailSize.GetWidth(), m_cameraSetupData.thumbnailSize.GetHeight())); 158 | frameData->SetThumbnail(new wxBitmap(m_cameraSetupData.thumbnailSize, 24)); 159 | ConvertMatBitmapTowxBitmap(matThumbnail, *frameData->GetThumbnail()); 160 | frameData->SetTimeToCreateThumbnail(stopWatch.Time()); 161 | } 162 | 163 | { 164 | wxCriticalSectionLocker locker(*m_cameraSetupData.framesCS); 165 | 166 | m_cameraSetupData.frames->push_back(std::move(frameData)); 167 | } 168 | 169 | if ( m_cameraSetupData.sleepDuration == CameraSetupData::SleepFromFPS ) 170 | { 171 | const wxLongLong timeSinceFrameCaptureStarted = wxGetUTCTimeMillis() - frameCaptureStartedTime; 172 | const long timeToSleep = msPerFrame - timeSinceFrameCaptureStarted.GetLo(); 173 | 174 | // exact time slept depends among else on the resolution of the system clock 175 | // for example, for MSW see Remarks in the ::Sleep() documentation at https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep 176 | if ( timeToSleep > 0 ) 177 | Sleep(timeToSleep); 178 | } 179 | else if ( m_cameraSetupData.sleepDuration == CameraSetupData::SleepNone ) 180 | { 181 | continue; 182 | } 183 | else if ( m_cameraSetupData.sleepDuration > 0 ) 184 | { 185 | Sleep(m_cameraSetupData.sleepDuration); 186 | } 187 | else 188 | { 189 | wxLogDebug("Invalid sleep duration %d", m_cameraSetupData.sleepDuration); 190 | } 191 | } 192 | else // connection to camera lost 193 | { 194 | m_isCapturing = false; 195 | m_cameraSetupData.eventSink->QueueEvent(new CameraEvent(EVT_CAMERA_ERROR_EMPTY, GetCameraName())); 196 | break; 197 | } 198 | } 199 | catch ( const std::exception& e ) 200 | { 201 | m_isCapturing = false; 202 | 203 | evt = new CameraEvent(EVT_CAMERA_ERROR_EXCEPTION, GetCameraName()); 204 | evt->SetString(e.what()); 205 | m_cameraSetupData.eventSink->QueueEvent(evt); 206 | 207 | break; 208 | } 209 | catch ( ... ) 210 | { 211 | m_isCapturing = false; 212 | 213 | evt = new CameraEvent(EVT_CAMERA_ERROR_EXCEPTION, GetCameraName()); 214 | evt->SetString("Unknown exception"); 215 | m_cameraSetupData.eventSink->QueueEvent(evt); 216 | 217 | break; 218 | } 219 | } 220 | 221 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Exiting CameraThread for camera '%s'...", GetCameraName()); 222 | return static_cast(nullptr); 223 | } 224 | 225 | 226 | bool CameraThread::InitCapture() 227 | { 228 | unsigned long cameraIndex = 0; 229 | 230 | if ( m_cameraSetupData.address.ToCULong(&cameraIndex) ) 231 | m_cameraCapture.reset(new cv::VideoCapture(cameraIndex, m_cameraSetupData.apiPreference)); 232 | else 233 | m_cameraCapture.reset(new cv::VideoCapture(m_cameraSetupData.address.ToStdString(), m_cameraSetupData.apiPreference)); 234 | 235 | return m_cameraCapture->isOpened(); 236 | } 237 | 238 | void CameraThread::SetCameraResolution(const wxSize& resolution) 239 | { 240 | if ( m_cameraCapture->set(cv::CAP_PROP_FRAME_WIDTH, resolution.GetWidth()) ) 241 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Set frame width to %d for camera '%s'", resolution.GetWidth(), GetCameraName()); 242 | else 243 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Could not set frame width to %d for camera '%s'", resolution.GetWidth(), GetCameraName()); 244 | 245 | if ( m_cameraCapture->set(cv::CAP_PROP_FRAME_HEIGHT, resolution.GetHeight()) ) 246 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Set frame height to %d for camera '%s'", resolution.GetHeight(), GetCameraName()); 247 | else 248 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Could not set frame height to %d for camera '%s'", resolution.GetHeight(), GetCameraName()); 249 | } 250 | 251 | void CameraThread::SetCameraUseMJPEG() 252 | { 253 | if ( m_cameraCapture->set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G')) ) 254 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Set FourCC to 'MJPG' for camera '%s'", GetCameraName()); 255 | else 256 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Could not set FourCC to 'MJPG' for camera '%s'", GetCameraName()); 257 | } 258 | 259 | void CameraThread::SetCameraFPS(const int FPS) 260 | { 261 | if ( m_cameraCapture->set(cv::CAP_PROP_FPS, FPS) ) 262 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Set FPS to %d for camera '%s'", FPS, GetCameraName()); 263 | else 264 | wxLogTrace(TRACE_WXOPENCVCAMERAS, "Could not set FPS to %d for camera '%s'", FPS, GetCameraName()); 265 | } 266 | 267 | 268 | void CameraThread::ProcessCameraCommand(const CameraCommandData& commandData) 269 | { 270 | CameraEvent* evt = new CameraEvent(EVT_CAMERA_COMMAND_RESULT, GetCameraName()); 271 | CameraCommandData evtCommandData; 272 | 273 | evtCommandData.command = commandData.command; 274 | 275 | if ( commandData.command == CameraCommandData::GetCameraInfo ) 276 | { 277 | CameraCommandData::CameraInfo cameraInfo; 278 | 279 | cameraInfo.threadSleepDuration = m_cameraSetupData.sleepDuration; 280 | cameraInfo.captureStartedTime = m_captureStartedTime; 281 | cameraInfo.framesCapturedCount = m_framesCapturedCount; 282 | cameraInfo.cameraCaptureBackendName = m_cameraCapture->getBackendName(); 283 | cameraInfo.cameraAddress = m_cameraSetupData.address; 284 | 285 | evtCommandData.parameter = cameraInfo; 286 | } 287 | else if ( commandData.command == CameraCommandData::SetThreadSleepDuration ) 288 | { 289 | m_cameraSetupData.sleepDuration = commandData.parameter.As(); 290 | 291 | evtCommandData.parameter = m_cameraSetupData.sleepDuration; 292 | } 293 | else if ( commandData.command == CameraCommandData::GetVCProp ) 294 | { 295 | const CameraCommandData::VCPropCommandParameters params = commandData.parameter.As(); 296 | CameraCommandData::VCPropCommandParameters evtParams; 297 | 298 | for ( const auto& p: params ) 299 | { 300 | CameraCommandData::VCPropCommandParameter evtParam; 301 | 302 | evtParam.id = p.id; 303 | evtParam.value = m_cameraCapture->get(p.id); 304 | evtParams.push_back(evtParam); 305 | } 306 | evtCommandData.parameter = evtParams; 307 | } 308 | else if ( commandData.command == CameraCommandData::SetVCProp ) 309 | { 310 | const CameraCommandData::VCPropCommandParameters params = commandData.parameter.As(); 311 | CameraCommandData::VCPropCommandParameters evtParams; 312 | 313 | for ( const auto& p: params ) 314 | { 315 | CameraCommandData::VCPropCommandParameter evtParam; 316 | 317 | evtParam.id = p.id; 318 | evtParam.value = p.value; 319 | evtParam.succeeded = m_cameraCapture->set(p.id, p.value); 320 | evtParams.push_back(evtParam); 321 | } 322 | evtCommandData.parameter = evtParams; 323 | } 324 | else 325 | { 326 | delete evt; 327 | wxFAIL_MSG("Invalid command"); 328 | return; 329 | } 330 | 331 | evt->SetPayload(evtCommandData); 332 | m_cameraSetupData.eventSink->QueueEvent(evt); 333 | } -------------------------------------------------------------------------------- /camerathread.h: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: camerathread.h 3 | // Purpose: Thread and event for retrieving images from a camera with cv::VideoCapture 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | 11 | #ifndef CAMERATHREAD_H 12 | #define CAMERATHREAD_H 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | // for wxLogTrace 25 | #define TRACE_WXOPENCVCAMERAS "WXOPENCVCAMERAS" 26 | 27 | /*********************************************************************************************** 28 | 29 | CameraCommandData: a struct used by the main thread to communicate with CameraThread. 30 | 31 | ***********************************************************************************************/ 32 | 33 | struct CameraCommandData 34 | { 35 | // see cv::VideoCapture get() and set() methods 36 | struct VCPropCommandParameter 37 | { 38 | int id{0}; 39 | double value{0.}; 40 | // only for SetVCProp, result of cv::VideoCapture::set() 41 | bool succeeded{false}; 42 | }; 43 | // for setting/getting multiple properties at once 44 | typedef std::vector VCPropCommandParameters; 45 | 46 | struct CameraInfo 47 | { 48 | long threadSleepDuration{0}; 49 | wxLongLong captureStartedTime{0}; 50 | wxULongLong framesCapturedCount{0}; 51 | wxString cameraCaptureBackendName; 52 | wxString cameraAddress; 53 | }; 54 | 55 | enum Commands 56 | { 57 | // parameter is CameraInfo 58 | GetCameraInfo = 0, 59 | 60 | // parameter is long, see CameraThread::m_sleepDuration 61 | SetThreadSleepDuration, 62 | 63 | // parameter is VCPropCommandParameters 64 | GetVCProp, 65 | SetVCProp, 66 | }; 67 | 68 | Commands command; 69 | wxAny parameter; 70 | }; 71 | 72 | typedef wxMessageQueue CameraCommandDatas; 73 | 74 | /*********************************************************************************************** 75 | 76 | CameraEvent: an event sent from the worker thread to the GUI thread. 77 | When type is EVT_CAMERA_ERROR_EXCEPTION, the exception information 78 | is available via its inherited GetString() method. 79 | 80 | ***********************************************************************************************/ 81 | 82 | class CameraEvent : public wxThreadEvent 83 | { 84 | public: 85 | // eventType is one of EVT_CAMERA_xxx types declared below 86 | CameraEvent(wxEventType eventType, const wxString& cameraName) 87 | : wxThreadEvent(eventType), m_cameraName(cameraName) 88 | {} 89 | 90 | wxString GetCameraName() const { return m_cameraName; } 91 | 92 | // only for EVT_CAMERA_COMMAND_RESULT 93 | CameraCommandData GetCommandResult() const { return GetPayload(); } 94 | 95 | wxEvent* Clone() const override { return new CameraEvent(*this); } 96 | protected: 97 | wxString m_cameraName; 98 | }; 99 | 100 | // Camera capture started 101 | // VideoCapture's backend can be retrieved via event's GetString(), 102 | // camera fps can be retrieved via event's GetInt(), if it returns non-zero 103 | wxDECLARE_EVENT(EVT_CAMERA_CAPTURE_STARTED, CameraEvent); 104 | // Result of the CameraCommandData's command sent to camera, use GetCommandResult() 105 | wxDECLARE_EVENT(EVT_CAMERA_COMMAND_RESULT, CameraEvent); 106 | // Could not open OpenCV camera capture 107 | wxDECLARE_EVENT(EVT_CAMERA_ERROR_OPEN, CameraEvent); 108 | // Could not retrieve a frame, consider connection to the camera lost. 109 | wxDECLARE_EVENT(EVT_CAMERA_ERROR_EMPTY, CameraEvent); 110 | // An exception was thrown in the camera thread, 111 | // see the event's GetString() for the exception information. 112 | wxDECLARE_EVENT(EVT_CAMERA_ERROR_EXCEPTION, CameraEvent); 113 | 114 | 115 | 116 | /*********************************************************************************************** 117 | 118 | CameraFrameData: a class containing information about a captured camera frame, 119 | created in a camera thread and processed in the GUI thread 120 | 121 | ***********************************************************************************************/ 122 | 123 | class CameraFrameData 124 | { 125 | public: 126 | CameraFrameData(const wxString& cameraName, 127 | const wxULongLong frameNumber); 128 | ~CameraFrameData(); 129 | 130 | wxString GetCameraName() const { return m_cameraName; } 131 | 132 | // captured camera frame 133 | wxBitmap* GetFrame() { return m_frame; } 134 | 135 | // optional thumbnail, created when thumbnailSize passed to CameraThread is not empty 136 | wxBitmap* GetThumbnail() { return m_thumbnail; } 137 | 138 | // frame number, starting with 0 139 | wxULongLong GetFrameNumber() const { return m_frameNumber; } 140 | 141 | // All times are in milliseconds 142 | 143 | // how long it took to retrieve the frame from OpenCV 144 | long GetTimeToRetrieve() const { return m_timeToRetrieve; } 145 | 146 | // how long it took to convert the frame from cv::Mat to wxBitmap 147 | long GetTimeToConvert() const { return m_timeToConvert; } 148 | 149 | // how long it took to resize and convert the frame to thumbnail 150 | long GetTimeToCreateThumbnail() const { return m_timeToCreateThumbnail; } 151 | 152 | // when was the image captured, obtained with wxGetUTCTimeMillis() 153 | wxLongLong GetCapturedTime() const { return m_capturedTime ; } 154 | 155 | // Setters 156 | 157 | void SetCameraName(const wxString& cameraName) { m_cameraName = cameraName; } 158 | 159 | void SetFrame(wxBitmap* frame) { m_frame = frame; } 160 | void SetThumbnail(wxBitmap* thumbnail) { m_thumbnail = thumbnail; } 161 | void SetFrameNumber(const wxULongLong number) { m_frameNumber = number; } 162 | 163 | void SetTimeToRetrieve(const long t) { m_timeToRetrieve = t; } 164 | void SetTimeToConvert(const long t) { m_timeToConvert = t; } 165 | void SetTimeToCreateThumbnail(const long t) { m_timeToCreateThumbnail = t; } 166 | void SetCapturedTime(const wxLongLong t) { m_capturedTime = t; } 167 | private: 168 | wxString m_cameraName; 169 | wxBitmap* m_frame{nullptr}; 170 | wxBitmap* m_thumbnail{nullptr}; 171 | wxULongLong m_frameNumber{0}; 172 | wxLongLong m_capturedTime{0}; 173 | long m_timeToRetrieve{0}; 174 | long m_timeToConvert{0}; 175 | long m_timeToCreateThumbnail{0}; 176 | }; 177 | 178 | typedef std::unique_ptr CameraFrameDataPtr; 179 | typedef std::vector CameraFrameDataPtrs; 180 | 181 | 182 | /*********************************************************************************************** 183 | 184 | CameraSetupData: a struct containing camera setup 185 | 186 | ***********************************************************************************************/ 187 | 188 | struct CameraSetupData 189 | { 190 | // how long the camera thread sleeps after grabbing the frame 191 | enum 192 | { 193 | SleepFromFPS = -1, // = (1000 / camera FPS ) - time taken to process the frame 194 | SleepNone = 0 // no sleep in the thread 195 | }; 196 | 197 | wxString name; 198 | wxString address; 199 | int apiPreference{0}; // = cv::CAP_ANY 200 | long sleepDuration{SleepFromFPS}; // either one of Sleep* or time in milliseconds 201 | int FPS{0}; // if 0 do not attempt to set 202 | int defaultFPS{25}; // when the camera FPS cannot be retrieved 203 | bool useMJPGFourCC{false}; 204 | 205 | // where to send EVT_CAMERA_xxx events; 206 | wxEvtHandler* eventSink{nullptr}; 207 | // new frames captured from camera, to be processed by the GUI thread 208 | CameraFrameDataPtrs* frames{nullptr}; 209 | wxCriticalSection* framesCS{nullptr}; 210 | wxSize frameSize; // if width or height is 0, not set 211 | wxSize thumbnailSize; 212 | 213 | // commands sent from the GUI thread to camera thread 214 | CameraCommandDatas* commands{nullptr}; 215 | 216 | bool IsOk() const; 217 | }; 218 | 219 | 220 | 221 | /*********************************************************************************************** 222 | 223 | CameraThread: a worker wxThread for retrieving images from a camera with OpenCV 224 | and sending them to GUI (a wxEvtHandler*) for display 225 | 226 | ***********************************************************************************************/ 227 | 228 | // forward declaration to avoid including OpenCV header 229 | namespace cv { class VideoCapture; } 230 | 231 | 232 | class CameraThread : public wxThread 233 | { 234 | public: 235 | CameraThread(const CameraSetupData& cameraSetupData); 236 | 237 | wxString GetCameraAddress() const { return m_cameraSetupData.address; } 238 | wxString GetCameraName() const { return m_cameraSetupData.name; } 239 | bool IsCapturing() const { return m_isCapturing; } 240 | protected: 241 | CameraSetupData m_cameraSetupData; 242 | 243 | std::unique_ptr m_cameraCapture; 244 | std::atomic_bool m_isCapturing{false}; 245 | wxLongLong m_captureStartedTime; // when was capture opened, obtained with wxGetUTCTimeMillis() 246 | wxULongLong m_framesCapturedCount{0}; 247 | 248 | ExitCode Entry() override; 249 | 250 | bool InitCapture(); 251 | void SetCameraResolution(const wxSize& resolution); 252 | void SetCameraUseMJPEG(); 253 | void SetCameraFPS(const int FPS); 254 | 255 | void ProcessCameraCommand(const CameraCommandData& commandData); 256 | }; 257 | 258 | #endif // #ifndef CAMERATHREAD_H -------------------------------------------------------------------------------- /convertmattowxbmp.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: convertmattowxbmp.cpp 3 | // Purpose: Converts OpenCV bitmap (Mat) stored as BGR CVU8 to wxBitmap 4 | // Author: PB 5 | // Created: 2020-09-16 6 | // Copyright: (c) 2020 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "convertmattowxbmp.h" 16 | 17 | #ifdef __WXMSW__ 18 | 19 | namespace 20 | { 21 | 22 | // Version optimized for Microsoft Windows. 23 | // matBitmap must be continous and matBitmap.cols % 4 must equal 0 24 | // as SetDIBits() requires the DIB rows to be DWORD-aligned. 25 | // Should not be called directly but only from ConvertMatBitmapTowxBitmap() 26 | // which does all the necessary debug checks. 27 | bool ConvertMatBitmapTowxBitmapMSW(const cv::Mat& matBitmap, wxBitmap& bitmap) 28 | { 29 | const HDC hScreenDC = ::GetDC(nullptr); 30 | BITMAPINFO bitmapInfo{0}; 31 | bool success; 32 | 33 | bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFO) - sizeof(RGBQUAD); 34 | bitmapInfo.bmiHeader.biWidth = bitmap.GetWidth(); 35 | bitmapInfo.bmiHeader.biHeight = 0 - bitmap.GetHeight(); 36 | bitmapInfo.bmiHeader.biPlanes = 1; 37 | bitmapInfo.bmiHeader.biBitCount = 24; 38 | bitmapInfo.bmiHeader.biCompression = BI_RGB; 39 | 40 | success = ::SetDIBits(hScreenDC, bitmap.GetHBITMAP(), 0, bitmap.GetHeight(), 41 | matBitmap.data, &bitmapInfo, DIB_RGB_COLORS) != 0; 42 | ::ReleaseDC(nullptr, hScreenDC); 43 | 44 | return success; 45 | } 46 | 47 | } // unnamed namespace 48 | 49 | #endif // #ifndef __WXMSW__ 50 | 51 | // See the function description in the header file. 52 | bool ConvertMatBitmapTowxBitmap(const cv::Mat& matBitmap, wxBitmap& bitmap) 53 | { 54 | wxCHECK(!matBitmap.empty(), false); 55 | wxCHECK(matBitmap.type() == CV_8UC3, false); 56 | wxCHECK(matBitmap.dims == 2, false); 57 | wxCHECK(bitmap.IsOk(), false); 58 | wxCHECK(bitmap.GetWidth() == matBitmap.cols && bitmap.GetHeight() == matBitmap.rows, false); 59 | wxCHECK(bitmap.GetDepth() == 24, false); 60 | 61 | #ifdef __WXMSW__ 62 | if ( bitmap.IsDIB() 63 | && matBitmap.isContinuous() 64 | && matBitmap.cols % 4 == 0 ) 65 | { 66 | return ConvertMatBitmapTowxBitmapMSW(matBitmap, bitmap); 67 | } 68 | #endif 69 | 70 | wxNativePixelData pixelData(bitmap); 71 | wxNativePixelData::Iterator pixelDataIt(pixelData); 72 | 73 | if ( matBitmap.isContinuous() ) 74 | { 75 | const uchar* bgr = matBitmap.data; 76 | 77 | for ( int row = 0; row < pixelData.GetHeight(); ++row ) 78 | { 79 | pixelDataIt.MoveTo(pixelData, 0, row); 80 | 81 | for ( int col = 0; 82 | col < pixelData.GetWidth(); 83 | ++col, ++pixelDataIt ) 84 | { 85 | pixelDataIt.Blue() = *bgr++; 86 | pixelDataIt.Green() = *bgr++; 87 | pixelDataIt.Red() = *bgr++; 88 | } 89 | } 90 | } 91 | else // Is it even possible for Mat with image to be not continous? 92 | { 93 | auto matBitmapIt = matBitmap.begin(); 94 | 95 | for ( int row = 0; row < pixelData.GetHeight(); ++row ) 96 | { 97 | pixelDataIt.MoveTo(pixelData, 0, row); 98 | 99 | for ( int col = 0; 100 | col < pixelData.GetWidth(); 101 | ++col, ++pixelDataIt, ++matBitmapIt ) 102 | { 103 | pixelDataIt.Blue() = (*matBitmapIt)[0]; 104 | pixelDataIt.Green() = (*matBitmapIt)[1]; 105 | pixelDataIt.Red() = (*matBitmapIt)[2]; 106 | } 107 | } 108 | } 109 | 110 | return bitmap.IsOk(); 111 | } -------------------------------------------------------------------------------- /convertmattowxbmp.h: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: convertmattowxbmp.h 3 | // Purpose: Converts OpenCV bitmap (Mat) stored as BGR CVU8 to wxBitmap 4 | // Author: PB 5 | // Created: 2020-09-16 6 | // Copyright: (c) 2020 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | 11 | #ifndef CONVERTMATTOWXBMP_H 12 | #define CONVERTMATTOWXBMP_H 13 | 14 | // forward declarations 15 | namespace cv { class Mat; } 16 | class wxBitmap; 17 | 18 | /** 19 | @param matBitmap 20 | Its data must be encoded as BGR CV_8UC3, which is 21 | probably the most common format for OpenCV images. 22 | @param bitmap 23 | It must be initialized to the same width and height as matBitmap 24 | and its depth must be 24. 25 | @return @true if the conversion succeeded, @false otherwise. 26 | 27 | 28 | On MS Windows, a MSW-optimized version is used if possible, 29 | the portable one otherwise. In my testing on MSW with 30 | 3840x2160 image in the Release build, the optimized version 31 | was about 25% faster then the portable one. MSW-optimized version 32 | is used when bitmap is a DIB and its width modulo 4 is 0. 33 | 34 | In my testing on MSW with MSVS using 3840x2160 image, the portable 35 | version of conversion function in the Debug build was more then 36 | 60 times slower than in the Release build. 37 | 38 | At least on MSW, initializing a wxBitmap takes quite some 39 | time. If you are processing images of the same size in a loop 40 | (e.g., frames of a video file), I recommend initializing the 41 | wxBitmap outside the loop and reusing it in the loop instead 42 | of creating it every time inside the loop. 43 | */ 44 | bool ConvertMatBitmapTowxBitmap(const cv::Mat& matBitmap, wxBitmap& bitmap); 45 | 46 | 47 | #endif // #ifndef CONVERTMATTOWXBMP_H -------------------------------------------------------------------------------- /onecameraframe.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: onecameraframe.cpp 3 | // Purpose: Displays full resolution output from a single camera 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | 11 | #include "onecameraframe.h" 12 | 13 | OneCameraFrame::OneCameraFrame(wxWindow* parent, const wxString& cameraName) 14 | : wxFrame(parent, wxID_ANY, cameraName) 15 | { 16 | m_cameraPanel = new CameraPanel(this, cameraName, true); 17 | m_cameraPanel->SetMinSize(wxSize(640, 400)); 18 | m_cameraPanel->SetMaxSize(wxSize(640, 400)); 19 | } 20 | 21 | void OneCameraFrame::SetCameraBitmap(const wxBitmap& bitmap, CameraPanel::Status status) 22 | { 23 | m_cameraPanel->SetBitmap(bitmap, status); 24 | 25 | if ( bitmap.IsOk() && !m_clientSizeAdjusted ) 26 | { 27 | m_cameraPanel->SetMinSize(bitmap.GetSize()); 28 | m_cameraPanel->SetMaxSize(bitmap.GetSize()); 29 | SetClientSize(bitmap.GetSize()); 30 | m_clientSizeAdjusted = true; 31 | SetTitle(wxString::Format("%s (resolution %dx%d)", 32 | GetCameraName(), bitmap.GetWidth(), bitmap.GetHeight())); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /onecameraframe.h: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Name: onecameraframe.h 3 | // Purpose: Displays full resolution output from a single camera 4 | // Author: PB 5 | // Created: 2021-11-18 6 | // Copyright: (c) 2021 PB 7 | // Licence: wxWindows licence 8 | /////////////////////////////////////////////////////////////////////////////// 9 | 10 | #ifndef ONECAMERAFRAME_H 11 | #define ONECAMERAFRAME_H 12 | 13 | #include 14 | 15 | #include "camerapanel.h" 16 | 17 | class OneCameraFrame : public wxFrame 18 | { 19 | public: 20 | OneCameraFrame(wxWindow* parent, const wxString& cameraName); 21 | 22 | void SetCameraBitmap(const wxBitmap& bitmap, CameraPanel::Status status = CameraPanel::Receiving); 23 | 24 | wxString GetCameraName() const { return m_cameraPanel->GetCameraName(); } 25 | 26 | private: 27 | bool m_clientSizeAdjusted{false}; 28 | CameraPanel* m_cameraPanel{nullptr}; 29 | }; 30 | 31 | #endif // #ifndef ONECAMERAFRAME_H -------------------------------------------------------------------------------- /screenshots/wxopencvcameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBfordev/wxOpenCVCameras/59b37c76be2fcf4cfce63ccbf3f2a1a1d7356282/screenshots/wxopencvcameras.png --------------------------------------------------------------------------------