├── .gitignore ├── LICENSE ├── README.rst ├── control.py ├── detect.py ├── docs ├── _static │ └── css │ │ └── wide.css ├── advanced.rst ├── build-requirements.txt ├── changelog.rst ├── conf.py ├── expanding.rst ├── graphics │ ├── button_icons.svg │ ├── icon.svg │ ├── icon16.png │ ├── icon256.png │ ├── icon32.png │ ├── icon48.png │ ├── logo.svg │ ├── make-icon.bat │ ├── schematics.svg │ └── splash.svg ├── index.rst ├── interface.rst ├── interface_activity.png ├── interface_camera_attributes.png ├── interface_camera_attributes_settings.png ├── interface_camera_settings.png ├── interface_camera_status.png ├── interface_extras.png ├── interface_filter.png ├── interface_footer.png ├── interface_image_display.png ├── interface_preferences.png ├── interface_processing.png ├── interface_save_control.png ├── interface_save_status.png ├── interface_save_trigger.png ├── interface_time_plot.png ├── interface_tutorial.png ├── logo.png ├── make-sphinx.py ├── overview.png ├── overview.rst ├── overview_compact.png ├── pipeline.rst ├── schematics.png ├── settings_file.rst ├── troubleshooting.rst └── usecases.rst ├── icon.ico ├── installdep.py ├── launcher ├── icon.rc ├── run-control-splash.c ├── run-control.c └── run-detect.c ├── pack.py ├── plugins ├── __init__.py ├── base.py ├── filter.py ├── filters │ ├── __init__.py │ ├── base.py │ ├── builtin.py │ ├── examples.py │ ├── profiler.py │ └── template.py ├── server.py └── trigger_save.py ├── requirements.txt ├── resources ├── cog.png ├── play.png ├── rec.png └── stop.png ├── run-device-server.py ├── settings.cfg ├── settings_deploy.cfg ├── splash.png ├── splash.py └── utils ├── __init__.py ├── cameras ├── AndorSDK2.py ├── AndorSDK3.py ├── Basler.py ├── Bonito.py ├── DCAM.py ├── IMAQdx.py ├── PCOSC2.py ├── PhotonFocus.py ├── ThorlabsTLCam.py ├── __init__.py ├── base.py ├── loader.py ├── picam.py ├── pvcam.py ├── sim.py └── uc480.py ├── gui ├── ActivityIndicator_ctl.py ├── DisplaySettings_ctl.py ├── FramePreprocess_ctl.py ├── FrameProcess_ctl.py ├── PlotControl_ctl.py ├── ProcessingIndicator_ctl.py ├── SaveBox_ctl.py ├── __init__.py ├── about.py ├── base_cam_ctl_gui.py ├── cam_attributes_browser.py ├── cam_gui_parameters.py ├── camera_control.py ├── color_theme.py ├── error_message.py ├── settings_editor.py └── tutorial.py └── services ├── __init__.py ├── dev.py ├── framestream.py └── misc.py /.gitignore: -------------------------------------------------------------------------------- 1 | logerr.txt 2 | logout.txt 3 | defaults*.cfg 4 | locals*.cfg 5 | settings-*.cfg 6 | 7 | local_python_cmd.bat 8 | local_python_interp_cmd.bat 9 | 10 | launcher/*.exe 11 | launcher/*.obj 12 | launcher/*.res 13 | 14 | Thumbs.db 15 | 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[co] 19 | *$py.class 20 | 21 | # VS code 22 | .vscode/ 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | env/ 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | .hypothesis/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # dotenv 101 | .env 102 | 103 | # virtualenv 104 | .venv 105 | venv/ 106 | ENV/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyLabLib cam-control: Software for universal camera control and frames acquisition 2 | ================================================================================== 3 | 4 | PyLabLib cam-control aims to provide a convenient interface for controlling cameras and acquiring their data in a unified way. 5 | 6 | .. image:: docs/overview.png 7 | 8 | 9 | Features 10 | ~~~~~~~~~~~~~~~~~~ 11 | 12 | - Communication with a variety of cameras: Andor, Hamamatsu, Thorlabs, PCO, PhotonFocus with IMAQ and Silicon Software frame grabbers. 13 | - Operating at high frame (>50 kFPS) and data (>300 Mb/s) rates. 14 | - On-line data processing and analysis: `binning `__, `background subtraction `__, simple built-in `image filters `__ (Gaussian blur, Fourier filtering), `playback slowdown `__ for fast process analysis. 15 | - Customization using `user-defined filters `__ (simple Python code operating on numpy arrays) or control from other software via a `TCP/IP interface `__. 16 | - Flexible data acquisition: `pre-trigger buffering `__, initiating acquisition on `timer, specific image property `__, or `external software signals `__. 17 | 18 | 19 | Installation 20 | ~~~~~~~~~~~~~~~~~~ 21 | 22 | To install cam-control, download the latest version from `GitHub `__ as a self-contained Zip file and then follow further `instructions `__ for how to run it. 23 | 24 | 25 | Documentation 26 | ~~~~~~~~~~~~~~~~~~ 27 | 28 | Detailed documentation is available at https://pylablib-cam-control.readthedocs.io/en/latest/ 29 | 30 | 31 | Related projects 32 | ~~~~~~~~~~~~~~~~~~ 33 | 34 | Cam-control is built using `pylablib `__, a more generic library for experiment automation and data acquisition, which included lots of additional `devices `__ besides cameras. -------------------------------------------------------------------------------- /detect.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Alexey Shkarin 2 | 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import sys 18 | import argparse 19 | if __name__=="__main__": 20 | os.chdir(os.path.join(".",os.path.split(sys.argv[0])[0])) 21 | sys.path.append(".") # set current folder to the file location and add it to the search path 22 | parser=argparse.ArgumentParser(description="Camera autodetection") 23 | parser.add_argument("--silent","-s",help="silent execution",action="store_true") 24 | parser.add_argument("--yes","-y",help="automatically confirm settings file overwrite",action="store_true") 25 | parser.add_argument("--show-errors",help="show errors raised on camera detection",action="store_true") 26 | parser.add_argument("--wait",help="show waiting message for 3 seconds in the end",action="store_true") 27 | parser.add_argument("--config-file","-cf", help="configuration file path",metavar="FILE",default="settings.cfg") 28 | args=parser.parse_args() 29 | if not args.silent: 30 | print("Detecting cameras...\n") 31 | 32 | from pylablib.core.utils import dictionary, general as general_utils 33 | from pylablib.core.fileio.loadfile import load_dict 34 | from pylablib.core.fileio.savefile import save_dict 35 | import pylablib 36 | 37 | import time 38 | import threading 39 | import datetime 40 | 41 | from utils.cameras import camera_descriptors 42 | 43 | ### Redirecting console / errors to file logs ### 44 | log_lock=threading.Lock() 45 | class StreamLogger(general_utils.StreamFileLogger): 46 | def __init__(self, path, stream=None): 47 | general_utils.StreamFileLogger.__init__(self,path,stream=stream,lock=log_lock) 48 | self.start_time=datetime.datetime.now() 49 | def write_header(self, f): 50 | f.write("\n\n"+"-"*50) 51 | f.write("\nStarting {} {:on %Y/%m/%d at %H:%M:%S}\n\n".format(os.path.split(sys.argv[0])[1],self.start_time)) 52 | sys.stderr=StreamLogger("logerr.txt",sys.stderr) 53 | sys.stdout=StreamLogger("logout.txt",sys.stdout) 54 | 55 | 56 | def detect_all(verbose=False): 57 | cams=dictionary.Dictionary() 58 | root_descriptors=[d for d in camera_descriptors.values() if d._expands is None] 59 | for c in root_descriptors: 60 | cams.update(c.detect(verbose=verbose,camera_descriptors=list(camera_descriptors.values())) or {}) 61 | if cams: 62 | for c in cams: 63 | if "display_name" not in cams[c]: 64 | cams[c,"display_name"]=c 65 | return dictionary.Dictionary({"cameras":cams}) 66 | 67 | 68 | def update_settings_file(cfg_path="settings.cfg", verbose=False, confirm=False, wait=False): 69 | settings=detect_all(verbose=verbose) 70 | if not settings: 71 | if verbose: print("Couldn't detect any supported cameras") 72 | else: 73 | do_save=True 74 | if os.path.exists(cfg_path): 75 | ans=input("Configuration file already exists. Modify? [y/N] ").strip() if confirm else "y" 76 | if ans.lower()!="y": 77 | do_save=False 78 | else: 79 | curr_settings=load_dict(cfg_path) 80 | if "cameras" in curr_settings: 81 | del curr_settings["cameras"] 82 | curr_settings["cameras"]=settings["cameras"] 83 | settings=curr_settings 84 | if do_save: 85 | save_dict(settings,cfg_path) 86 | if verbose: print("Successfully generated config file {}".format(cfg_path)) 87 | else: 88 | return 89 | if confirm and not do_save: 90 | input() 91 | elif wait: 92 | time.sleep(3.) 93 | 94 | if __name__=="__main__": 95 | if os.path.exists(args.config_file): 96 | settings=load_dict(args.config_file) 97 | if "dlls" in settings: 98 | for k,v in settings["dlls"].items(): 99 | pylablib.par["devices/dlls",k]=v 100 | if args.silent: 101 | verbose=False 102 | else: 103 | verbose="full" if args.show_errors else True 104 | update_settings_file(cfg_path=args.config_file,verbose=verbose,confirm=not (args.silent or args.yes),wait=args.wait) -------------------------------------------------------------------------------- /docs/_static/css/wide.css: -------------------------------------------------------------------------------- 1 | /* wider page */ 2 | .wy-nav-content { 3 | max-width: none !important; 4 | } 5 | 6 | /* override table width restrictions */ 7 | @media screen and (min-width: 767px) { 8 | .wy-table-responsive table td { 9 | white-space: normal !important; /* enable wrapping */ 10 | } 11 | .wy-table-responsive { 12 | overflow: visible !important; /* don't add horizontal scroll */ 13 | } 14 | .wy-table-responsive colgroup col { 15 | width: auto !important; /* auto column width */ 16 | } 17 | } -------------------------------------------------------------------------------- /docs/advanced.rst: -------------------------------------------------------------------------------- 1 | .. _advanced: 2 | 3 | Advanced features 4 | ========================= 5 | 6 | 7 | .. _advanced_slowdown: 8 | 9 | Playback slowdown 10 | ------------------------- 11 | 12 | Fast cameras are typically used to analyze fast events. However, cam-control still has to display data in real time, which means that this fast acquisition potential is lost during on-line analysis. Of course, it still saves all the frames for further examination, but it usually takes some time to load and go through them. 13 | 14 | For a quick check, there is an option to temporarily slow data display to the desired frame rate, slowing down the playback. For example, if the camera operates at 10 kFPS and the playback is set to work at 100 FPS, the process is seen 100 times slower. 15 | 16 | The way it works is by storing all incoming camera frames into a temporary buffer, and then taking these frames from the buffer at a lower rate. Since the size of this buffer is limited, the slowdown naturally can only proceed for a finite time. It is easy to calculate that, e.g., for the same 10 kFPS camera speed and 100 FPS playback speed the buffer of 1000 frames will take 0.1s of real time and stretch it into 10s of display time. 17 | 18 | Note that the slowdowns happens after the pre-binning, but before filters and background subtraction. Hence, it affects all of the displayed frames, but not the saving, which still happens at the regular rate. 19 | 20 | This feature controls are on the :ref:`Processing tab `. 21 | 22 | 23 | .. _advanced_time_plot: 24 | 25 | Time plot 26 | ------------------------- 27 | 28 | Sometimes it is useful to look at how the image values evolve in time. Cam-control has basic capabilities for plotting the mean value of the frame or a rectangular ROI within it as a function of time or frame number. It can be set in two slightly different ways: either plot averages of displayed frames vs. time, or averages of all camera frames vs. frame index. 29 | 30 | This feature is only intended for a quick on-line data assessment, so there is currently no provided way to save these plots. As an alternative, you can either save the whole move, or use :ref:`time map filter ` and save the resulting frame. 31 | 32 | This feature controls are on the :ref:`Processing tab `. 33 | 34 | 35 | .. _advanced_save_trigger: 36 | 37 | Saving trigger 38 | ------------------------- 39 | 40 | Often we would like to automate data acquisition. There are two basic built-in ways to do that in cam-control. 41 | 42 | The first is simple timer automation, where a new data set is acquired with a given period. It is useful when monitoring relatively slow processes, when recording data continuously is excessive. 43 | 44 | The second is based on the acquired images themselves. Specifically, it is triggered when any pixel in a displayed image goes above a certain threshold value. Since multiple consecutive frames can trigger saving, this method also includes a dead time: a time after triggering during which all triggers are ignored. This way, the resulting datasets can be spaced wider in time, if required. However, even with zero dead time (or zero period for timer trigger) the recording can only start after the previous recording is finished, so that each saved dataset is complete. 45 | 46 | The image-based method strongly benefits from two other software features: :ref:`pre-trigger buffer ` and :ref:`filters `. The first one allows to effectively start saving some time before the triggering image, to make sure that the data preceding the event is also recorded. The second one adds a lot of flexibility to the exact triggering conditions. Generally, it is pretty rare that one is really interested in the brightest pixel value. Using filters, you can transform image to make the brightest pixel value more relevant (e.g., use transform to better highlight particles, or use temporal variations to catch the moment when the image starts changing a lot), or even create a "fake" filter output a single-pixel 0 or 1 image, whose sole job is to trigger the acquisition. 47 | 48 | Both timed and image trigger also support a couple common features. They both can trigger either standard save for more thorough data acquisition, or snapshot to get a quick assessment. And both can take a limit on the total number of saving events. 49 | 50 | This feature controls are on the :ref:`Plugins tab `. 51 | 52 | 53 | .. _advanced_filter: 54 | 55 | Filters 56 | ------------------------- 57 | 58 | Filters provide a flexible way to perform on-line image processing. They can be used to quickly assess the data in real time or even to :ref:`automate data acquisition `. 59 | 60 | They are primarily designed for :ref:`expanding by users `. Nevertheless, there are several pre-made filters covering some basic spatial and temporal image transforms: 61 | 62 | - **Gaussian blur**: standard image blur, i.e., spatial low-pass filter. The only parameter is the blur size. 63 | - **FFT filter**: Fourier domain filter, which is a generalization of Gaussian filter. It involves both low-pass ("minimal size") and high-pass ("maximal size") filtering, and can be implemented either using a hard cutoff in the Fourier space, or as a Gaussian, which is essentially equivalent to the Gaussian filter above. 64 | - **Moving average**: average several consecutive frames within a sliding window together. It is conceptually similar to :ref:`time pre-binning `, but only affects the displayed frames and works within a sliding window. It is also possible to take only every n'th frame (given by ``Period`` parameter) to cover larger time span without increasing the computational load. 65 | - **Moving accumulator**: a more generic version of moving average. Works very similarly, but can apply several different combination methods in addition to averaging: taking per-pixel median, min, max, or standard deviation (i.e., plot how much each pixel's value fluctuates in time). 66 | - **Moving average subtraction**: combination of the moving average and the time derivative. Averages frames in two consecutive sliding windows and displays their difference. Can be thought of as a combination of a moving average and a sliding :ref:`background subtraction `. This approach was used to enhance sensitivity of single protein detection in interferometric scattering microscopy (iSCAT) [Young2018]_, and it is described in detail in [Dastjerdi2021]_. 67 | - **Time map**: a 2D map which plots a time evolution of a line cut. The cut can be taken along either direction and possibly averaged over several rows or columns. For convenience, the ``Frame`` display mode shows the frames with only the averaged part visible. This filter is useful to examine some time trends in the data in more details than the simple local average plot. 68 | - **Difference matrix**: a map for pairwise frames differences. Shows a map ``M[i,j]``, where each element is the RMS difference between ``i``'th and ``j``'th frames. This is useful for examining the overall image evolution and spot, e.g., periodic disturbances or switching behavior. 69 | 70 | This feature controls are on the :ref:`Filter tab `. 71 | 72 | .. [Young2018] Gavin Young et al., `"Quantitative mass imaging of single biological macromolecules," `__ *Science* **360**, 423-427 (2018) 73 | 74 | .. [Dastjerdi2021] Houman Mirzaalian Dastjerdi, Mahyar Dahmardeh, André Gemeinhardt, Reza Gholami Mahmoodabadi, Harald Köstler, and Vahid Sandoghdar, `"Optimized analysis for sensitive detection and analysis of single proteins via interferometric scattering microscopy," `__ *bioRxiv doi*: `10.1101/2021.08.16.456463 `__ -------------------------------------------------------------------------------- /docs/build-requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/build-requirements.txt -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | Release history 4 | ============================ 5 | 6 | `Version 2.2.1 `__ 7 | --------------------------------------------------------------------------------------------------------------------------------------------- 8 | 9 | 2022-10-05 10 | 11 | * Added Basler pylon-compatible camera, BitFlow frame grabbers with PhotonFocus cameras, and support for AlliedVision Bonito cameras with National Instruments frame grabbers. 12 | * Added basic beam profiler filter. 13 | * Minor bugfixes. 14 | 15 | `Download `__ 16 | 17 | | 18 | 19 | `Version 2.2.0 `__ 20 | --------------------------------------------------------------------------------------------------------------------------------------------- 21 | 22 | 2022-04-04 23 | 24 | * Added Photometrics cameras. 25 | * Added camera attributes browser to Andor SDK3, Hamamatsu, Photometrics, Princeton Instruments, and IMAQdx cameras. 26 | * Added lines coordinate systems to the plotter, added ROI selection in the image. 27 | * Added a separate event logging window for a more convenient logging of larger messages. 28 | * Added single-shot data recording for high-performance saving. 29 | * Altered the save trigger behavior: now the new save can start only after the previous is done. 30 | * Minor bugfixes. 31 | 32 | `Download `__ 33 | 34 | | 35 | 36 | `Version 2.1.2 `__ 37 | --------------------------------------------------------------------------------------------------------------------------------------------- 38 | 39 | 2021-12-23 40 | 41 | * Graphics and UI update: added launcher executables, "About" windows, popup error messages. 42 | 43 | `Download `__ 44 | 45 | | 46 | 47 | `Version 2.1.1 `__ 48 | --------------------------------------------------------------------------------------------------------------------------------------------- 49 | 50 | 2021-12-12 51 | 52 | * Added preferences editor. 53 | * Added option for importing settings from a previous software version. 54 | * Added creation of camera-specific shortcuts. 55 | * Minor GUI bugfixes. 56 | 57 | `Download `__ 58 | 59 | | 60 | 61 | `Version 2.1.0 `__ 62 | --------------------------------------------------------------------------------------------------------------------------------------------- 63 | 64 | 2021-12-04 65 | 66 | * Added tutorial. 67 | * Added activity indicator. 68 | * Implemented color themes. 69 | * Added IDS uEye and Princeton Instruments cameras. 70 | * Slightly reorganized GUI. 71 | * Minor bug fixes, wider cameras support. 72 | 73 | `Download `__ 74 | 75 | | 76 | 77 | `Version 2.0.1 `__ 78 | --------------------------------------------------------------------------------------------------------------------------------------------- 79 | 80 | 2021-10-05 81 | 82 | * Changed event log format: added recorded frame number and column name header. 83 | * Reorganized snapshot saving controls. 84 | * Added processing steps indicators above the image plots. 85 | * Minor bug fixes. 86 | 87 | `Download `__ 88 | 89 | | 90 | 91 | `Version 2.0.0 `__ 92 | --------------------------------------------------------------------------------------------------------------------------------------------- 93 | 94 | 2021-09-29 95 | 96 | Major upgrade, first publicly released version. 97 | 98 | `Download `__ -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'pylablib cam-control' 23 | copyright = '2021, Alexey Shkarin' 24 | author = 'Alexey Shkarin' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '2.2.1' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.githubpages', 43 | ] 44 | 45 | 46 | nitpicky=True 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | # 63 | # This is also used if you do content translation via gettext catalogs. 64 | # Usually you set "language" from the command line for these cases. 65 | language = None 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | # This pattern also affects html_static_path and html_extra_path. 70 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 71 | 72 | # The name of the Pygments (syntax highlighting) style to use. 73 | pygments_style = 'sphinx' 74 | 75 | 76 | # -- Options for HTML output ------------------------------------------------- 77 | 78 | # The theme to use for HTML and HTML Help pages. See the documentation for 79 | # a list of builtin themes. 80 | # 81 | # html_theme = 'alabaster' 82 | html_theme = 'sphinx_rtd_theme' 83 | 84 | # Theme options are theme-specific and customize the look and feel of a theme 85 | # further. For a list of options available for each theme, see the 86 | # documentation. 87 | # 88 | # html_theme_options = {} 89 | 90 | # Add any paths that contain custom static files (such as style sheets) here, 91 | # relative to this directory. They are copied after the builtin static files, 92 | # so a file named "default.css" will overwrite the builtin "default.css". 93 | html_static_path = ['_static'] 94 | 95 | # def setup(app): 96 | # app.add_stylesheet('css/wide.css') 97 | 98 | 99 | # Custom sidebar templates, must be a dictionary that maps document names 100 | # to template names. 101 | # 102 | # The default sidebars (for documents that don't match any pattern) are 103 | # defined by theme itself. Builtin themes are using these templates by 104 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 105 | # 'searchbox.html']``. 106 | # 107 | # html_sidebars = {} 108 | 109 | 110 | # -- Options for HTMLHelp output --------------------------------------------- 111 | 112 | # Output file base name for HTML help builder. 113 | htmlhelp_basename = 'cam-control-doc' 114 | 115 | html_logo = "logo.png" 116 | 117 | # -- Options for LaTeX output ------------------------------------------------ 118 | 119 | latex_elements = { 120 | # The paper size ('letterpaper' or 'a4paper'). 121 | # 122 | # 'papersize': 'letterpaper', 123 | 124 | # The font size ('10pt', '11pt' or '12pt'). 125 | # 126 | # 'pointsize': '10pt', 127 | 128 | # Additional stuff for the LaTeX preamble. 129 | # 130 | # 'preamble': '', 131 | 132 | # Latex figure (float) alignment 133 | # 134 | # 'figure_align': 'htbp', 135 | } 136 | 137 | # Grouping the document tree into LaTeX files. List of tuples 138 | # (source start file, target name, title, 139 | # author, documentclass [howto, manual, or own class]). 140 | latex_documents = [ 141 | (master_doc, 'cam-control.tex', 'pylablib cam-control Documentation', 142 | 'Alexey Shkarin', 'manual'), 143 | ] 144 | 145 | 146 | # -- Options for manual page output ------------------------------------------ 147 | 148 | # One entry per manual page. List of tuples 149 | # (source start file, name, description, authors, manual section). 150 | man_pages = [ 151 | (master_doc, 'cam-control', 'pylablib cam-control Documentation', 152 | [author], 1) 153 | ] 154 | 155 | 156 | # -- Options for Texinfo output ---------------------------------------------- 157 | 158 | # Grouping the document tree into Texinfo files. List of tuples 159 | # (source start file, target name, title, author, 160 | # dir menu entry, description, category) 161 | texinfo_documents = [ 162 | (master_doc, 'cam-control', 'pylablib cam-control Documentation', 163 | author, 'cam-control', 'Software for universal camera control and acquisition.', 164 | 'Miscellaneous'), 165 | ] 166 | 167 | 168 | # -- Options for Epub output ------------------------------------------------- 169 | 170 | # Bibliographic Dublin Core info. 171 | epub_title = project 172 | 173 | # The unique identifier of the text. This can be a ISBN number 174 | # or the project homepage. 175 | # 176 | # epub_identifier = '' 177 | 178 | # A unique identification for the text. 179 | # 180 | # epub_uid = '' 181 | 182 | # A list of files that should not be packed into the epub file. 183 | epub_exclude_files = ['search.html'] 184 | 185 | 186 | # -- Extension configuration ------------------------------------------------- 187 | -------------------------------------------------------------------------------- /docs/graphics/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/graphics/icon16.png -------------------------------------------------------------------------------- /docs/graphics/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/graphics/icon256.png -------------------------------------------------------------------------------- /docs/graphics/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/graphics/icon32.png -------------------------------------------------------------------------------- /docs/graphics/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/graphics/icon48.png -------------------------------------------------------------------------------- /docs/graphics/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 71 | 82 | 93 | cam-control 108 | 117 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/graphics/make-icon.bat: -------------------------------------------------------------------------------- 1 | magick convert icon16.png icon32.png icon48.png icon256.png ../../icon.ico -------------------------------------------------------------------------------- /docs/graphics/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 62 | 73 | 81 | 89 | cam-control 101 | 107 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | PyLabLib cam-control: Software for universal camera control and frames acquisition 2 | ================================================================================== 3 | 4 | PyLabLib cam-control aims to provide a convenient interface for controlling cameras and acquiring their data in a unified way. 5 | 6 | .. image:: overview.png 7 | 8 | Features: 9 | 10 | - Communication with a variety of cameras: Andor, Hamamatsu, Thorlabs, IDS, PCO, Photometrics, Princeton Instruments, PhotonFocus with IMAQ and Silicon Software frame grabbers. 11 | - Operating at high frame (100 kFPS) and data (1 Gb/s) rates. 12 | - On-line data processing and analysis: :ref:`binning `, :ref:`background subtraction `, simple built-in :ref:`image filters ` (Gaussian blur, Fourier filtering), :ref:`playback slowdown ` for fast process analysis. 13 | - Customization using :ref:`user-defined filters ` (simple Python code operating on numpy arrays) or control from other software via a :ref:`TCP/IP interface `. 14 | - Flexible data acquisition: :ref:`pre-trigger buffering `, initiating acquisition on :ref:`timer, specific image property `, or :ref:`external software signals `. 15 | 16 | To install cam-control, download the latest version from `GitHub `__ as a self-contained Zip file and then follow further :ref:`instructions ` for how to run it. 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | :includehidden: 21 | :caption: Contents: 22 | 23 | overview 24 | pipeline 25 | advanced 26 | interface 27 | expanding 28 | settings_file 29 | usecases 30 | troubleshooting 31 | changelog -------------------------------------------------------------------------------- /docs/interface_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_activity.png -------------------------------------------------------------------------------- /docs/interface_camera_attributes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_camera_attributes.png -------------------------------------------------------------------------------- /docs/interface_camera_attributes_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_camera_attributes_settings.png -------------------------------------------------------------------------------- /docs/interface_camera_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_camera_settings.png -------------------------------------------------------------------------------- /docs/interface_camera_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_camera_status.png -------------------------------------------------------------------------------- /docs/interface_extras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_extras.png -------------------------------------------------------------------------------- /docs/interface_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_filter.png -------------------------------------------------------------------------------- /docs/interface_footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_footer.png -------------------------------------------------------------------------------- /docs/interface_image_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_image_display.png -------------------------------------------------------------------------------- /docs/interface_preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_preferences.png -------------------------------------------------------------------------------- /docs/interface_processing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_processing.png -------------------------------------------------------------------------------- /docs/interface_save_control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_save_control.png -------------------------------------------------------------------------------- /docs/interface_save_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_save_status.png -------------------------------------------------------------------------------- /docs/interface_save_trigger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_save_trigger.png -------------------------------------------------------------------------------- /docs/interface_time_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_time_plot.png -------------------------------------------------------------------------------- /docs/interface_tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/interface_tutorial.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/logo.png -------------------------------------------------------------------------------- /docs/make-sphinx.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import argparse 3 | import shutil 4 | 5 | 6 | def clear_build(): 7 | shutil.rmtree("_build",ignore_errors=True) 8 | 9 | 10 | def make(builder="html"): 11 | subprocess.call(["sphinx-build","-M",builder,".","_build"]) 12 | 13 | 14 | parser=argparse.ArgumentParser() 15 | parser.add_argument("-c","--clear",action="store_true") 16 | args=parser.parse_args() 17 | 18 | if args.clear: 19 | print("Clearing build\n") 20 | clear_build() 21 | make() -------------------------------------------------------------------------------- /docs/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/overview.png -------------------------------------------------------------------------------- /docs/overview_compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/overview_compact.png -------------------------------------------------------------------------------- /docs/schematics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/docs/schematics.png -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | .. _troubleshooting: 2 | 3 | Troubleshooting 4 | ========================= 5 | 6 | - **Camera is not detected by the software** 7 | 8 | - Camera is disconnected, turned off, its drivers are missing, or it is used by a different program: Andor Solis, Hokawo, NI MAX, PFRemote, PCO Camware, ThorCam, etc. Check if it can be opened in its native software. 9 | - Software can not find the libraries. Make sure that the :ref:`native camera software ` is installed in the default path, or :ref:`manually specify the DLL paths `. 10 | - Frame grabber cameras and IMAQdx cameras currently have only limited support. Please, :ref:`contact the developer ` to see if it is possible to add your specific camera to the software. 11 | 12 | - **Camera camera controls are disabled and camera status says "Disconnected"** 13 | 14 | - Camera is disconnected, turned off, its drivers are missing, or it is used by a different program. Check if it can be opened in its native software. 15 | 16 | - **The image is not updated when the camera acquisition is running** 17 | 18 | - Make sure that you are looking at the correct image tab, and that the ``Update`` button is pressed. 19 | - The camera is in the external trigger mode and no triggering signal is incoming. Switch the camera to the internal trigger mode. 20 | - In some cases certain camera setting combinations result in unstable behavior. Check if these exact camera settings work in the native software. 21 | 22 | - **The operation or frames update is slow or laggy** 23 | 24 | - Frame plotting takes up too much resources. Reduce the frame update period or, if necessary, turn off the display update. 25 | - Additional plotting features take up too much resources. Turn off frame cuts plotting, color histogram, and switch to the plain grayscale color scheme. 26 | - On-line processing takes up too much resources. Turn off pre-binning, background subtraction, filters (disable or unload), and mean frame plots. 27 | - Slowdown is turned on, camera frame rate is low, or time frame binning is enabled with a large factor. All of these can lead to actually low generated frame rate. 28 | - Selected display update period (selected under the plot parameters) is too large. 29 | - By default, the camera is polled for new frames every 50ms, so the frame rate is limited by 20 FPS. To increase it, you can specify the ``loop/min_poll_period`` parameter the :ref:`settings file `. 30 | 31 | - **Acquisition is frozen** 32 | - Acquisition is still being set up, which can take up to several seconds in some cases. This can be confirmed by ``Acquisition`` camera status saying ``Setting up...``. 33 | - Camera buffer overflow on Andor SDK3 cameras (e.g., Zyla) can sometimes lead to such behavior. Reduce the data transfer rate using smaller frame rate, smaller ROI or larger binning, and then restart the acquisition. 34 | - Some NI IMAQ frame grabbers can freeze if the camera data transfer rate is too high (~200Mb/s). In this case it will freeze the acquisition shortly after start. This can be confirmed by the disagreement between the expected frame rate from the ``Frame period`` indicator and the actual frame rate from the ``FPS`` camera status. If this is the case, reduce the data transfer rate using smaller frame rate, smaller ROI or larger binning, and then restart the acquisition. 35 | 36 | - **Missing frames are reported after saving** 37 | 38 | - Acquisition can not deal with high data or frame rate. Check if the :ref:`frame buffer ` is full or constantly increasing. If so, reduce the frame rate or frame size. 39 | - Frame info acquisition takes too much time. On some cameras (e.g., uc480 and Silicon Software frame grabbers) acquiring frame information can take a significant fraction of the frame readout, especially for small frames and high readout rates. If this is the case, you need to turn the frame info off. 40 | - Frames pre-binning can not deal with high data rate. Check if the frame buffer is full or constantly increasing. If so, reduce the frame rate or frame size, or turn the pre-binning off. 41 | - Software as a whole can not deal with high data or frame rate. Minimize the load from unnecessary programs running on this PC. Avoid using remote control software (e.g., TeamViewer or Windows Remote Desktop) during the saving sessions. 42 | - Acquisition has been restarted during saving (e.g., due to parameters change). 43 | - PhotonFocus cameras can generate frames faster than some frame grabbers (e.g., SiliconSoftware microEnable 4) can manage. This shows up as lower frame rate than expected from the frame period. If this is the case, reduce the frame rate. 44 | - The data rate is higher than the drive writing rate, and the :ref:`save buffer ` is overflown. Reduce the size of a single saving session, switch to a faster drive (SSD), or increase the save buffer size. 45 | - (especially if missing frames occur right at the beginning) Obtaining camera settings for saving takes too much time. Increase the buffer size using ``misc/buffer/min_size/time`` parameter in the :ref:`settings file `, or turn off the settings file (not recommended). 46 | 47 | - **Saving in Tiff format ends abruptly or produces corrupted files** 48 | 49 | - Tiff format does not support files larger than 2 Gb. Either split data in smaller files (e.g., using the :ref:`file split ` settings), or use other format such as BigTiff. 50 | 51 | - **Control window is too large and does not fit into the screen** 52 | 53 | - You can enable the compact mode in the :ref:`setting file `. 54 | 55 | - **Camera performance is lower than can be achieved in the native software** 56 | 57 | - Make sure that all available settings (including advanced settings such as readout speed, pixel clock, etc.) are the same in both cases. 58 | - Some specific cameras might not be fully supported. Please, :ref:`contact the developer ` to add necessary settings of your specific camera to the software. -------------------------------------------------------------------------------- /docs/usecases.rst: -------------------------------------------------------------------------------- 1 | .. _usecases: 2 | 3 | Use cases 4 | ========================= 5 | 6 | Here we describe some basic use cases and applications of the cam-control software. 7 | 8 | - **Standard acquisition** 9 | 10 | This is the standard mode which requires minimal initial configuration. Simply setup the :ref:`camera parameters `, configure the :ref:`saving `, start the acquisition, and press ``Saving``. 11 | 12 | - **Recording of rare events** 13 | 14 | Manual recording of rare and fast events is often challenging. Usually you have to either record everything and sift through the data later, or hope to press ``Saving`` quickly enough to catch most of it. Neither option is ideal: the first method takes a lot of extra storage space, while the second requires fast reaction. :ref:`Pretrigger buffer ` takes the best of both worlds: it still allows to only record interesting parts, put lets you start saving a couple seconds before the button is pressed, so it is not necessary to press it as quickly as possible. The buffer is set up in the :ref:`saving parameters `. 15 | 16 | - **Periodic acquisition** 17 | 18 | To record very slow processes, you might want to just occasionally take a snapshot or a short movie. You can use :ref:`saving trigger ` for that. 19 | 20 | - **Image-based acquisition start** 21 | 22 | Another :ref:`saving trigger ` application is to automate data acquisition. It gives an option to start acquisition based on the frame values. Combined with :ref:`custom filters `, it provides a powerful way to completely automate acquisition of interesting events. 23 | 24 | - **Background subtraction** 25 | 26 | Oftentimes the camera images contain undesired static or slowly changing background. It is possible to get rid of it using the built-in basic :ref:`background subtraction `. Keep in mind that it only affects the displayed data (and, as such, is not applied to frames supplied to filters). 27 | 28 | - **On-line image processing** 29 | 30 | More complicated on-line processing is available through filters. Cam-control already comes with several basic :ref:`built-in filters `, but the real power in custom application comes from the ability to easily write :ref:`custom filters `. 31 | 32 | - **On-line analysis of fast processes** 33 | 34 | To do a quick on-line analysis of fast processes, you can use the :ref:`frame slowdown ` capability. 35 | 36 | - **Interfacing with external software** 37 | 38 | Cam-control provides a :ref:`TCP/IP server control ` which allows one to control GUI, start or stop acquisition and saving, and directly acquire frames from the camera. Since this is implemented as a server, it is platform- and software-independent, so it can interface with any custom software written in, e.g., C++, Java, LabView, or Matlab. It can be used to synchronize data acquisition with other devices, implement more sophisticated automation, or provide frames for custom image-processing software. 39 | 40 | - **Controlling several cameras** 41 | 42 | Cam-control allows for control of several connected cameras. If more than one camera is specified in the settings file (typically these are found by running the ``detect`` script), then every time the software is started, it allows to select which camera to control. Several instances can run simultaneously for different cameras without interference. The default GUI parameters are stored independently for all cameras. 43 | 44 | The selection window can be avoided by creating a specific camera shortcut in the :ref:`extras menu `. It creates a shortcut which uses the ``--camera`` :ref:`argument ` (e.g., run ``control.exe --camera ppimaq_0``) to supply the specific camera name. -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/icon.ico -------------------------------------------------------------------------------- /installdep.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import time 5 | 6 | def get_executable(console=False): 7 | """ 8 | Get Python executable. 9 | 10 | If ``console==True`` and the current executable is windowed (i.e., ``"pythonw.exe"``), return the corresponding ``"python.exe"`` instead. 11 | """ 12 | folder,file=os.path.split(sys.executable) 13 | if file.lower()=="pythonw.exe" and console: 14 | return os.path.join(folder,"python.exe") 15 | return sys.executable 16 | def pip_install(pkg, upgrade=False): 17 | """ 18 | Call ``pip install`` for a given package. 19 | 20 | If ``upgrade==True``, call with ``--upgrade`` key (upgrade current version if it is already installed). 21 | """ 22 | if upgrade: 23 | subprocess.call([get_executable(console=True), "-m", "pip", "install", "--upgrade", pkg]) 24 | else: 25 | subprocess.call([get_executable(console=True), "-m", "pip", "install", pkg]) 26 | def install_wheel(path, upgrade=True, verbose=True): 27 | """Install a wheel file with the given path, if it exists""" 28 | if verbose: 29 | print("Installing {}".format(path)) 30 | if os.path.exists(path): 31 | pip_install(path,upgrade=upgrade) 32 | elif verbose: 33 | print("Could not find file {}".format(path)) 34 | 35 | 36 | def main(): 37 | install_wheel("BFModule-1.0.1-cp38-cp38-win_amd64.whl") 38 | time.sleep(3.) 39 | 40 | if __name__=="__main__": 41 | main() -------------------------------------------------------------------------------- /launcher/icon.rc: -------------------------------------------------------------------------------- 1 | id ICON "..\icon.ico" -------------------------------------------------------------------------------- /launcher/run-control-splash.c: -------------------------------------------------------------------------------- 1 | #pragma comment(linker, "/SUBSYSTEM:windows /ENTRY:mainCRTStartup") 2 | 3 | #include 4 | #include 5 | 6 | #define CLLEN 4096 7 | 8 | 9 | int main(int argc, char *argv[]) { 10 | char call_argv[CLLEN]; 11 | strcpy_s(call_argv,CLLEN,"python\\pythonw.exe cam-control\\splash.py"); 12 | for (int i=1;i 4 | #include 5 | 6 | #define CLLEN 4096 7 | 8 | 9 | int main(int argc, char *argv[]) { 10 | char call_argv[CLLEN]; 11 | strcpy_s(call_argv,CLLEN,"python\\python.exe cam-control\\control.py"); 12 | for (int i=1;i 4 | #include 5 | 6 | #define CLLEN 4096 7 | 8 | 9 | int main(int argc, char *argv[]) { 10 | char call_argv[CLLEN]; 11 | strcpy_s(call_argv,CLLEN,"python\\python.exe cam-control\\detect.py"); 12 | for (int i=1;iself.p["length"]: 76 | nl=len(self.buffer)//self.p["length"] 77 | last_buffer=self.buffer[(nl-1)*self.p["length"]:nl*self.p["length"]] 78 | self.buffer=self.buffer[nl*self.p["length"]:] 79 | self._latest_frame=np.mean(last_buffer,axis=0) 80 | self.p["buff_accum"]=len(self.buffer) 81 | def generate_frame(self): 82 | if self._latest_frame is not None: 83 | result=self._latest_frame 84 | self._latest_frame=None 85 | self._bufflen=len(self.buffer) 86 | return result 87 | 88 | 89 | 90 | 91 | class MovingAverageFilter(base.IMultiFrameFilter): 92 | """ 93 | Filter that generates moving average (averages last ``self.p["length"]`` received frames). 94 | 95 | Identical to :class:`FastMovingAverageFilter` from ``builtin`` module, but a but slower; 96 | not used in the GUI, left for reference. 97 | """ 98 | # _class_name="moving_avg" # class is only for illustration purposes 99 | _class_caption="Moving average" 100 | _class_description="Averages a given number of consecutive frames into a single frame. Frames are averaged within a sliding window." 101 | def setup(self): 102 | super().setup(process_incomplete=True) 103 | self.add_parameter("length",label="Number of frames",kind="int",limit=(1,None),default=20) 104 | self.add_parameter("period",label="Frame step",kind="int",limit=(1,None),default=1) 105 | def set_parameter(self, name, value): 106 | super().set_parameter(name,value) 107 | buffer_size=value if name=="length" else None 108 | buffer_step=value if name=="period" else None 109 | self.reshape_buffer(buffer_size,buffer_step) 110 | def process_buffer(self, buffer): 111 | if not buffer: 112 | return None 113 | return np.mean(buffer,axis=0) 114 | 115 | 116 | 117 | 118 | class MovingAverageSubtractionFilter(base.IMultiFrameFilter): 119 | """ 120 | Filter that generate moving average difference. 121 | 122 | Finds the difference between the average of the last ``self.p["length"]`` received frames and the average of the preceding ``self.p["length"]`` frames 123 | 124 | Identical to :class:`FastMovingAverageSubtractionFilter` from ``builtin`` module, but a but slower; 125 | not used in the GUI, left for reference. 126 | """ 127 | # _class_name="moving_avg_sub" # class is only for illustration purposes 128 | _class_caption="Moving average subtract" 129 | _class_description=("Averages two consecutive frame blocks into two individual frames and takes their difference. " 130 | "Similar to running background subtraction, but with some additional time averaging.") 131 | def setup(self): 132 | super().setup() 133 | self.add_parameter("length",label="Number of frames",kind="int",limit=(1,None),default=20) 134 | self.add_parameter("period",label="Frame step",kind="int",limit=(1,None),default=1) 135 | def set_parameter(self, name, value): 136 | super().set_parameter(name,value) 137 | buffer_size=value*2 if name=="length" else None 138 | buffer_step=value if name=="period" else None 139 | self.reshape_buffer(buffer_size,buffer_step) 140 | def process_buffer(self, buffer): 141 | return np.mean(buffer[0:self.p["length"]],axis=0)-np.mean(buffer[self.p["length"]:2*self.p["length"]],axis=0) 142 | -------------------------------------------------------------------------------- /plugins/filters/profiler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gaussian beam profiler filter. 3 | """ 4 | 5 | 6 | from . import base 7 | from pylablib.core.dataproc import fitting 8 | 9 | import numpy as np 10 | 11 | 12 | 13 | class BeamProfileFilter(base.ISingleFrameFilter): 14 | """ 15 | Beam profiler filter. 16 | """ 17 | _class_name="beam_profile" 18 | _class_caption="Beam profile" 19 | _class_description="Beam profiler filter: averages image in strips of the given widths in vertical and horizontal directions, fits the resulting profiles to Gaussians and shows the widths" 20 | def setup(self): 21 | """Initial filter setup""" 22 | super().setup(multichannel="average") 23 | # Setup control parameters 24 | self.add_parameter("x_position",label="X position",kind="int",limit=(0,None)) 25 | self.add_parameter("y_position",label="Y position",kind="int",limit=(0,None)) 26 | self.add_parameter("track_lines",label="Use plot lines",kind="check") 27 | self.add_parameter("track_max",label="Locate maximum",kind="check") 28 | self.add_parameter("width",label="Averaging width",kind="int",limit=(1,None),default=10) 29 | self.add_parameter("show_map_info",label="Showing",kind="select",options={"frame":"Frame","data":"Data profile","fit":"Fit profile"}) 30 | # Add width indicators 31 | self.add_parameter("x_fit_width",label="X width",kind="float",indicator=True) 32 | self.add_parameter("y_fit_width",label="Y width",kind="float",indicator=True) 33 | # Add auxiliary parameters 34 | self.add_linepos_parameter(default=None) # indicate that the filter needs to get a cross position as "linepos" parameter 35 | self.add_rectangle("x_selection",(0,0),(0,0)) # add a rectangle indicating x-cut area 36 | self.add_rectangle("y_selection",(0,0),(0,0)) # add a rectangle indicating y-cut area 37 | self.select_plotter("frame") 38 | def set_parameter(self, name, value): # called automatically any time a GUI parameter or an image cross position are changed 39 | """Set filter parameter with the given name""" 40 | super().set_parameter(name,value) # default parameter set (store the value in ``self.p`` dictionary) 41 | if name in ["linepos","track_lines","show_map_info"] and self.p["show_map_info"]=="frame" and self.p["track_lines"] and self.p["linepos"]: 42 | self.set_parameter("x_position",int(self.p["linepos"][1])) 43 | self.set_parameter("y_position",int(self.p["linepos"][0])) 44 | def _get_region(self, shape): 45 | """Get the spans ``(start, stop)`` of the two averaging regions""" 46 | xp,yp,w=self.p["x_position"],self.p["y_position"],self.p["width"] 47 | def _get_rng(ax, p): 48 | start,stop=(p-w//2),(p-w//2+w) 49 | size=shape[ax] 50 | start=max(start,0) 51 | stop=min(stop,size) 52 | start=max(min(start,stop-w),0) 53 | stop=min(max(stop,w),size) 54 | return start,stop 55 | return _get_rng(0,yp),_get_rng(1,xp) 56 | def profile(self, xs, center, width, height, background): 57 | """Profile fit function""" 58 | return np.exp(-(xs-center)**2/(2*width**2))*height+background 59 | def fit_profile(self, cut): 60 | """ 61 | Fit the beam profile. 62 | 63 | Return tuple ``(fit_parameters, fit_cut)``, where ``fit_parameters`` is a dictionary with the resulting fit parameters, 64 | and ``fit_cut`` is a fit to the given profile cut. 65 | """ 66 | xs=np.arange(len(cut)) 67 | fitter=fitting.Fitter(self.profile,"xs") 68 | background=np.median(cut) 69 | fit_parameters={"center":cut.argmax(),"width":len(cut)/10,"background":background,"height":cut.max()-background} 70 | fp,ff=fitter.fit(xs,cut,fit_parameters=fit_parameters) 71 | return fp,ff(xs) 72 | def process_frame(self, frame): # called automatically whenver a new frame is received from the camera 73 | """Process a new camera frame""" 74 | if self.p["track_max"]: # move center to the image maximum, if enabled 75 | imax,jmax=np.unravel_index(frame.argmax(),frame.shape) 76 | self.set_parameter("x_position",jmax) 77 | self.set_parameter("y_position",imax) 78 | # Extract profiles 79 | rs,cs=self._get_region(frame.shape) 80 | xcut=np.mean(frame[rs[0]:rs[1],:],axis=0) 81 | xcut/=xcut.max() 82 | ycut=np.mean(frame[:,cs[0]:cs[1]],axis=1) 83 | ycut/=ycut.max() 84 | # Fit profiles 85 | xfp,xfcut=self.fit_profile(xcut) 86 | self.p["x_fit_width"]=xfp["width"] 87 | yfp,yfcut=self.fit_profile(ycut) 88 | self.p["y_fit_width"]=yfp["width"] 89 | if self.p["show_map_info"]=="frame": # showing the original frames 90 | rs,cs=self._get_region(frame.shape) 91 | nr,nc=frame.shape 92 | # Set parameters of the rectangles indicating the profile extraction areas 93 | self.change_rectangle("x_selection",center=((rs[1]+rs[0])/2,nc/2),size=(rs[1]-rs[0],nc),visible=True) 94 | self.change_rectangle("y_selection",center=(nr/2,(cs[1]+cs[0])/2),size=(nr,cs[1]-cs[0]),visible=True) 95 | self.select_plotter("frame") 96 | return frame 97 | if self.p["show_map_info"]=="data": # showing the extracted profiles 98 | return xcut[None,:]*ycut[:,None] 99 | return xfcut[None,:]*yfcut[:,None] # showing the fit profiles -------------------------------------------------------------------------------- /plugins/filters/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example template module for custom-defined filters. 3 | """ 4 | 5 | 6 | from . import base 7 | 8 | import numpy as np 9 | import scipy.ndimage 10 | 11 | 12 | 13 | class TemplateSingleFrameFilter(base.ISingleFrameFilter): 14 | """ 15 | Template filter which does nothing. 16 | """ 17 | # _class_name="template" # NOTE: uncomment this line to enable the filter 18 | _class_caption="Template single-frame filter" 19 | _class_description="This is a template single-frame filter, which is equivalent to the standard Gaussian blur filter" 20 | def setup(self): 21 | super().setup() 22 | self.add_parameter("width",label="Width",limit=(0,None),default=2) 23 | def process_frame(self, frame): 24 | return scipy.ndimage.gaussian_filter(frame.astype("float"),self.p["width"]) 25 | 26 | 27 | 28 | class TemplateMultiFrameFilter(base.IMultiFrameFilter): 29 | """ 30 | Template filter which does nothing. 31 | """ 32 | # _class_name="template" # NOTE: uncomment this line to enable the filter 33 | _class_caption="Template multi-frame filter" 34 | _class_description="This is a template multi-frame filter, which simply returns the average of all frames" 35 | def setup(self): 36 | super().setup(process_incomplete=True) 37 | self.add_parameter("length",label="Number of frames",kind="int",limit=(1,None),default=20) 38 | self.add_parameter("period",label="Frame step",kind="int",limit=(1,None),default=1) 39 | def set_parameter(self, name, value): 40 | super().set_parameter(name,value) 41 | buffer_size=value if name=="length" else None 42 | buffer_step=value if name=="period" else None 43 | self.reshape_buffer(buffer_size,buffer_step) 44 | def process_buffer(self, buffer): 45 | if not buffer: 46 | return None 47 | return np.mean(buffer,axis=0) -------------------------------------------------------------------------------- /plugins/trigger_save.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | from pylablib.core.thread import controller 3 | # from pylablib.devices import NI, Conrad 4 | 5 | import numpy as np 6 | import time 7 | 8 | 9 | class TriggerSavePlugin(base.IPlugin): 10 | """ 11 | Plugin for automatic starting of saving either on timer, or on maximal value of a plotted frame. 12 | """ 13 | _class_name="trigger_save" 14 | _default_start_order=10 15 | def setup(self): 16 | self._last_save_timer=None 17 | self._last_save_image=None 18 | self._acquired_videos=None 19 | self._trigger_display_time=0.5 20 | self.trig_modes=["timer","image"] 21 | self._frame_sources={} 22 | self.extctls["resource_manager"].cs.add_resource("process_activity","saving/"+self.full_name,ctl=self.ctl, 23 | caption="Trigger save",short_cap="Trg",order=10) 24 | self.setup_gui_sync() 25 | self.ctl.subscribe_commsync(lambda *args: self._update_frame_sources(), 26 | srcs=self.extctls["resource_manager"].name,tags=["resource/added","resource/removed"]) 27 | self._update_frame_sources(reset_value=True) 28 | self.ctl.add_job("check_timer_trigger",self.check_timer_trigger,0.1) 29 | self.ctl.add_command("toggle",self.toggle) 30 | self.ctl.v["enabled"]=False 31 | 32 | def setup_gui(self): 33 | self.table=self.gui.add_plugin_box("params","Save trigger",cache_values=True) 34 | self.table.add_combo_box("save_mode",options={"full":"Full","snap":"Snap"},label="Save mode") 35 | self.table.add_check_box("limit_videos",caption="Limit number of videos",value=False) 36 | self.table.add_num_edit("max_videos",1,limiter=(1,None,"coerce","int"),formatter="int",label="Number of videos",add_indicator=True) 37 | trig_mode_names={"timer":"Timer","image":"Image"} 38 | self.table.add_combo_box("trigger_mode",options={m:trig_mode_names[m] for m in self.trig_modes},label="Trigger mode") 39 | self.table.add_num_edit("period",10,limiter=(.1,None,"coerce"),formatter=("float","auto",1),label="Timer period (s)") 40 | self.table.add_combo_box("frame_source",options=[],label="Trigger frame source") 41 | self.table.add_num_edit("image_trigger_threshold",0,formatter=("float","auto",4),label="Trigger threshold") 42 | self.table.add_num_edit("dead_time",10,limiter=(0,None,"coerce"),formatter=("float","auto",1),label="Dead time (s)") 43 | self.table.add_text_label("event_trigger_status","armed",label="Event trigger status: ") 44 | self.table.add_toggle_button("enabled","Enabled",value=False) 45 | self.table.vs["limit_videos"].connect(lambda v: self.table.set_enabled("max_videos",v)) 46 | self.table.set_enabled("max_videos",False) 47 | @controller.exsafe 48 | def reset_acquired_videos(): 49 | self.table.i["max_videos"]=self._acquired_videos=0 50 | self._last_video=False 51 | self.table.vs["enabled"].connect(reset_acquired_videos) 52 | reset_acquired_videos() 53 | @controller.exsafe 54 | def setup_gui_state(): 55 | trigger_mode=self.table.v["trigger_mode"] 56 | self.table.set_enabled("period",trigger_mode=="timer") 57 | self.table.set_enabled("dead_time",trigger_mode!="timer") 58 | self.table.set_enabled("frame_source",trigger_mode=="image") 59 | self.table.set_enabled("image_trigger_threshold",trigger_mode=="image") 60 | self.table.set_enabled("enabled",not (trigger_mode=="image" and self.table.v["frame_source"]==-1)) 61 | self._update_trigger_status("armed") 62 | self.table.vs["trigger_mode"].connect(setup_gui_state) 63 | self.table.vs["frame_source"].connect(setup_gui_state) 64 | setup_gui_state() 65 | 66 | def _update_frame_sources(self, update_subscriptions=True, reset_value=None): 67 | sources=self.extctls["resource_manager"].cs.list_resources("frame/display") 68 | sources={n:v for n,v in sources.items() if "src" in v and "tag" in v} 69 | if update_subscriptions: 70 | for n in list(self._frame_sources): 71 | if n not in sources: 72 | self.ctl.unsubscribe(self._frame_sources.pop(n)) 73 | def make_frame_recv_func(src): 74 | return lambda s,t,m: self.check_frame_trigger(src,m.last_frame()) 75 | for n,v in sources.items(): 76 | if n not in self._frame_sources: 77 | sid=self.ctl.subscribe_commsync(make_frame_recv_func(n),srcs=v["src"],tags=v["tag"],limit_queue=1) 78 | self._frame_sources[n]=sid 79 | self._update_frame_sources_indicator(sources,reset_value=reset_value) 80 | @controller.call_in_gui_thread 81 | def _update_frame_sources_indicator(self, sources, reset_value=False): 82 | index_values,options=zip(*[(n,v.get("caption",n)) for n,v in sources.items()]) 83 | self.table.w["frame_source"].set_options(options=options,index_values=index_values,index=0 if reset_value else None) 84 | @controller.call_in_gui_thread 85 | def _start_save(self, mode): 86 | self.guictl.call_thread_method("toggle_saving",mode=mode,start=True,no_popup=True) 87 | self._acquired_videos+=1 88 | self.table.i["max_videos"]=self._acquired_videos 89 | if self._acquired_videos>=self.table.v["max_videos"] and self.table.v["limit_videos"]: 90 | self._last_video=True 91 | def _saving_in_progress(self): 92 | saving_status=self.extctls["resource_manager"].cs.get_resource("process_activity","saving/streaming").get("status","off") 93 | return saving_status!="off" 94 | def check_timer_trigger(self): 95 | """Check saving timer and start saving if it's passed""" 96 | enabled=self.table.v["enabled"] 97 | self.ctl.v["enabled"]=enabled 98 | if enabled and self.table.v["trigger_mode"]=="timer": 99 | t=time.time() 100 | if not (self._saving_in_progress() or self._last_video) and (self._last_save_timer is None or t>self._last_save_timer+self.table.v["period"]): 101 | self._start_save(self.table.v["save_mode"]) 102 | self._last_save_timer=t 103 | else: 104 | self._last_save_timer=None 105 | self.extctls["resource_manager"].csi.update_resource("process_activity","saving/"+self.full_name,status="on" if enabled else "off") 106 | if self._last_video and not self._saving_in_progress(): 107 | self.toggle(enable=False) 108 | def _update_trigger_status(self, status): 109 | if self.table.v["event_trigger_status"]!=status: # check (cached) value first to avoid unnecessary calls to GUI thread 110 | self.table.v["event_trigger_status"]=status 111 | def check_frame_trigger(self, src, frame): 112 | """Check incoming image and start saving if it's passed""" 113 | dead_time=self.table.v["dead_time"] if self.table.v["enabled"] else 0 114 | t=time.time() 115 | if self.table.v["trigger_mode"]=="image": 116 | if self.table.v["frame_source"]==src: 117 | if self._last_save_image is None or t>self._last_save_image+dead_time: 118 | if np.any(frame>self.table.v["image_trigger_threshold"]): 119 | if not (self._saving_in_progress() or self._last_video) and self.table.v["enabled"]: 120 | self._start_save(self.table.v["save_mode"]) 121 | self._last_save_image=t 122 | if self._last_save_image is not None and tsv2: 13 | return ">" 14 | elif sv11 else "")) 99 | for i in range(cam_num): 100 | try: 101 | if verbose: print("Found Andor SDK2 camera idx={}".format(i)) 102 | with Andor.AndorSDK2Camera(idx=i) as cam: 103 | device_info=cam.get_device_info() 104 | if verbose: print("\tModel {}".format(device_info.head_model)) 105 | yield cam,None 106 | except Andor.AndorError: 107 | if verbose=="full": cls.print_error() 108 | @classmethod 109 | def generate_description(cls, idx, cam=None, info=None): 110 | device_info=cam.get_device_info() 111 | cam_desc=cls.build_cam_desc(params={"idx":idx}) 112 | cam_desc["display_name"]="Andor {} {}".format(device_info.head_model,device_info.serial_number) 113 | cam_name="andor_sdk2_{}".format(idx) 114 | return cam_name,cam_desc 115 | 116 | def get_kind_name(self): 117 | return "Generic Andor SDK2" 118 | 119 | def make_thread(self, name): 120 | return AndorSDK2CameraThread(name=name,kwargs=self.settings["params"].as_dict()) 121 | 122 | def make_gui_control(self, parent): 123 | return Settings_GUI(parent,cam_desc=self) 124 | def make_gui_status(self, parent): 125 | return GenericCameraStatus_GUI(parent,cam_desc=self) 126 | 127 | 128 | 129 | class AndorSDK2IXONCameraDescriptor(AndorSDK2CameraDescriptor): 130 | _cam_kind="AndorSDK2IXON" 131 | _expands="AndorSDK2" 132 | @classmethod 133 | def generate_description(cls, idx, cam=None, info=None): 134 | if cam.get_capabilities()["cam_type"]=="AC_CAMERATYPE_IXON": 135 | return super().generate_description(idx,cam=cam,info=info) 136 | def get_kind_name(self): 137 | return "Andor iXON" 138 | def make_thread(self, name): 139 | return AndorSDK2IXONThread(name=name,kwargs=self.settings["params"].as_dict()) 140 | def make_gui_control(self, parent): 141 | return IXONCameraSettings_GUI(parent,cam_desc=self) 142 | def make_gui_status(self, parent): 143 | return IXONCameraStatus_GUI(parent,cam_desc=self) 144 | 145 | 146 | 147 | class AndorSDK2LucaCameraDescriptor(AndorSDK2CameraDescriptor): 148 | _cam_kind="AndorSDK2Luca" 149 | _expands="AndorSDK2" 150 | @classmethod 151 | def generate_description(cls, idx, cam=None, info=None): 152 | if cam.get_capabilities()["cam_type"]=="AC_CAMERATYPE_LUCA": 153 | return super().generate_description(idx,cam=cam,info=info) 154 | def get_kind_name(self): 155 | return "Andor Luca" 156 | def make_thread(self, name): 157 | return AndorSDK2LucaThread(name=name,kwargs=self.settings["params"].as_dict()) 158 | def make_gui_control(self, parent): 159 | return LucaCameraSettings_GUI(parent,cam_desc=self) 160 | def make_gui_status(self, parent): 161 | return LucaCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/AndorSDK3.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import Andor 2 | from pylablib.thread.devices.Andor import AndorSDK3CameraThread, AndorSDK3ZylaThread, AndorSDK3NeoThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui import cam_gui_parameters, cam_attributes_browser 6 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 7 | 8 | 9 | 10 | class CamAttributesBrowser(cam_attributes_browser.CamAttributesBrowser): 11 | def _add_attribute(self, name, attribute, value): 12 | if not attribute.readable: 13 | return 14 | indicator=not attribute.writable 15 | if attribute.kind=="int": 16 | self._record_attribute(name,"int",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 17 | self.add_integer_parameter(name,attribute.name,limits=(attribute.min,attribute.max),indicator=indicator) 18 | elif attribute.kind=="float": 19 | self._record_attribute(name,"float",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 20 | self.add_float_parameter(name,attribute.name,limits=(attribute.min,attribute.max),indicator=indicator) 21 | elif attribute.kind=="enum": 22 | if attribute.values: 23 | self._record_attribute(name,"enum",attribute,indicator=indicator,rng=attribute.ilabels) 24 | self.add_choice_parameter(name,attribute.name,attribute.ilabels,indicator=indicator) 25 | elif attribute.kind=="str": 26 | self._record_attribute(name,"str",attribute,indicator=indicator) 27 | self.add_string_parameter(name,attribute.name,indicator=indicator) 28 | elif attribute.kind=="bool": 29 | self._record_attribute(name,"bool",attribute,indicator=indicator) 30 | self.add_bool_parameter(name,attribute.name,indicator=indicator) 31 | def _get_attribute_range(self, attribute): 32 | if attribute.kind in ["int","float"]: 33 | return (attribute.min,attribute.max) 34 | if attribute.kind=="enum": 35 | return attribute.ilabels 36 | 37 | class Settings_GUI(GenericCameraSettings_GUI): 38 | _bin_kind="both" 39 | _frame_period_kind="value" 40 | def setup_settings_tables(self): 41 | super().setup_settings_tables() 42 | self.add_parameter(cam_gui_parameters.AttributesBrowserGUIParameter(self,CamAttributesBrowser),"advanced") 43 | 44 | class Status_GUI(GenericCameraStatus_GUI): 45 | def setup_status_table(self): 46 | super().setup_status_table() 47 | self.add_num_label("buffer_overflows",formatter="int",label="Buffer overflows:") 48 | def show_parameters(self, params): 49 | super().show_parameters(params) 50 | if "missed_frames" in params: 51 | self.v["buffer_overflows"]=params["missed_frames"].overflows 52 | self.w["buffer_overflows"].setStyleSheet("font-weight: bold" if params["missed_frames"].overflows else "") 53 | 54 | class StatusCooled_GUI(Status_GUI): 55 | def setup_status_table(self): 56 | super().setup_status_table() 57 | self.add_num_label("temperature_monitor",formatter=("float","auto",1,True),label="Temperature (C):") 58 | def show_parameters(self, params): 59 | super().show_parameters(params) 60 | self.v["temperature_monitor"]=params.get("temperature_monitor","N/A") 61 | 62 | 63 | 64 | 65 | class AndorSDK3CameraDescriptor(ICameraDescriptor): 66 | _cam_kind="AndorSDK3" 67 | 68 | @classmethod 69 | def iterate_cameras(cls, verbose=False): 70 | if verbose: print("Searching for Andor SDK3 cameras") 71 | try: 72 | cam_num=Andor.get_cameras_number_SDK3() 73 | except (Andor.AndorError, OSError): 74 | if verbose: print("Error loading or running the Andor SDK3 library: required software (Andor Solis) must be missing\n") 75 | if verbose=="full": cls.print_error() 76 | return 77 | if not cam_num: 78 | if verbose: print("Found no Andor SDK3 cameras\n") 79 | return 80 | if verbose: print("Found {} Andor SDK3 camera{}".format(cam_num,"s" if cam_num>1 else "")) 81 | for i in range(cam_num): 82 | try: 83 | if verbose: print("Found Andor SDK3 camera idx={}".format(i)) 84 | with Andor.AndorSDK3Camera(idx=i) as cam: 85 | device_info=cam.get_device_info() 86 | if verbose: print("\tModel {}".format(device_info.camera_model)) 87 | yield cam,None 88 | except Andor.AndorError: 89 | if verbose=="full": cls.print_error() 90 | @classmethod 91 | def generate_description(cls, idx, cam=None, info=None): 92 | device_info=cam.get_device_info() 93 | cam_desc=cls.build_cam_desc(params={"idx":idx}) 94 | cam_desc["display_name"]="Andor {} {}".format(device_info.camera_model,device_info.serial_number) 95 | cam_name="andor_sdk3_{}".format(idx) 96 | return cam_name,cam_desc 97 | 98 | def get_kind_name(self): 99 | return "Generic Andor SDK3" 100 | 101 | def make_thread(self, name): 102 | return AndorSDK3CameraThread(name=name,kwargs=self.settings["params"].as_dict()) 103 | 104 | def make_gui_control(self, parent): 105 | return Settings_GUI(parent,cam_desc=self) 106 | def make_gui_status(self, parent): 107 | return StatusCooled_GUI(parent,cam_desc=self) 108 | 109 | 110 | 111 | 112 | class AndorSDK3ZylaCameraDescriptor(AndorSDK3CameraDescriptor): 113 | _cam_kind="AndorSDK3Zyla" 114 | _expands="AndorSDK3" 115 | @classmethod 116 | def generate_description(cls, idx, cam=None, info=None): 117 | if cam.get_device_info().camera_name.lower().startswith("zyla"): 118 | return super().generate_description(idx,cam=cam,info=info) 119 | def get_kind_name(self): 120 | return "Andor Zyla" 121 | def make_thread(self, name): 122 | return AndorSDK3ZylaThread(name=name,kwargs=self.settings["params"].as_dict()) 123 | 124 | class AndorSDK3NeoCameraDescriptor(AndorSDK3CameraDescriptor): 125 | _cam_kind="AndorSDK3Neo" 126 | _expands="AndorSDK3" 127 | @classmethod 128 | def generate_description(cls, idx, cam=None, info=None): 129 | if cam.get_device_info().camera_name.lower().startswith("neo"): 130 | return super().generate_description(idx,cam=cam,info=info) 131 | def get_kind_name(self): 132 | return "Andor Neo" 133 | def make_thread(self, name): 134 | return AndorSDK3NeoThread(name=name,kwargs=self.settings["params"].as_dict()) -------------------------------------------------------------------------------- /utils/cameras/Basler.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import Basler 2 | from pylablib.thread.devices.Basler import BaslerPylonCameraThread 3 | from pylablib.core.thread import controller 4 | 5 | from .base import ICameraDescriptor 6 | from ..gui import cam_gui_parameters, cam_attributes_browser 7 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 8 | 9 | 10 | 11 | class CamAttributesBrowser(cam_attributes_browser.CamAttributesBrowser): 12 | def setup(self, cam_ctl): 13 | super().setup(cam_ctl) 14 | with self.buttons.using_layout("buttons"): 15 | self.buttons.add_combo_box("visibility",label="Visibility",options={"simple":"Simple","intermediate":"Intermediate","advanced":"Advanced","invisible":"Full"},value="simple",location=(0,0)) 16 | self.buttons.vs["visibility"].connect(self.setup_visibility) 17 | def _add_attribute(self, name, attribute, value): 18 | if not attribute.readable: 19 | return 20 | indicator=not attribute.writable 21 | if attribute.kind=="int": 22 | self._record_attribute(name,"int",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 23 | self.add_integer_parameter(name,attribute.display_name,limits=(attribute.min,attribute.max),indicator=indicator) 24 | elif attribute.kind=="float": 25 | self._record_attribute(name,"float",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 26 | self.add_float_parameter(name,attribute.display_name,limits=(attribute.min,attribute.max),indicator=indicator) 27 | elif attribute.kind=="enum": 28 | self._record_attribute(name,"enum",attribute,indicator=indicator,rng=attribute.ilabels) 29 | self.add_choice_parameter(name,attribute.display_name,attribute.ilabels,indicator=indicator) 30 | elif attribute.kind=="str": 31 | self._record_attribute(name,"str",attribute,indicator=indicator) 32 | self.add_string_parameter(name,attribute.display_name,indicator=indicator) 33 | elif attribute.kind=="bool": 34 | self._record_attribute(name,"bool",attribute,indicator=indicator) 35 | self.add_bool_parameter(name,attribute.display_name,indicator=indicator) 36 | def _get_attribute_range(self, attribute): 37 | if attribute.kind in ["int","float"]: 38 | return (attribute.min,attribute.max) 39 | if attribute.kind=="enum": 40 | return attribute.ilabels 41 | @controller.exsafe 42 | def setup_visibility(self): 43 | quick=self.buttons.v["quick_access"] 44 | vis=self.buttons.v["visibility"] 45 | vis_order=["simple","intermediate","advanced","invisible","unknown"] 46 | for n in self._attributes: 47 | vis_pass=vis_order.index(self._attributes[n].attribute.visibility)<=vis_order.index(vis) 48 | self._show_attribute(n,(not quick or self.props_table.v["p_quick",n]) and vis_pass) 49 | def setup_parameters(self, full_info): 50 | super().setup_parameters(full_info) 51 | self.setup_visibility() 52 | 53 | 54 | 55 | class Settings_GUI(GenericCameraSettings_GUI): 56 | _frame_period_kind="value" 57 | def setup_settings_tables(self): 58 | super().setup_settings_tables() 59 | self.add_parameter(cam_gui_parameters.AttributesBrowserGUIParameter(self,CamAttributesBrowser),"advanced") 60 | 61 | 62 | 63 | class BaslerPylonCameraDescriptor(ICameraDescriptor): 64 | _cam_kind="BaslerPylon" 65 | 66 | @classmethod 67 | def iterate_cameras(cls, verbose=False): 68 | if verbose: print("Searching for Basler cameras") 69 | try: 70 | cams=Basler.list_cameras() 71 | except (Basler.BaslerError, OSError, AttributeError): 72 | if verbose: print("Error loading or running the Basler library: required software (Basler pylon) must be missing\n") 73 | if verbose=="full": cls.print_error() 74 | return 75 | if len(cams)==0: 76 | if verbose: print("Found no Basler cameras\n") 77 | return 78 | cam_num=len(cams) 79 | if verbose: print("Found {} Basler camera{}".format(cam_num,"s" if cam_num>1 else "")) 80 | for i,cdesc in enumerate(cams): 81 | if verbose: print("Checking Basler camera idx={}\n\tVendor {}, model {}".format(i,cdesc.vendor,cdesc.model)) 82 | yield None,cdesc 83 | @classmethod 84 | def generate_description(cls, idx, cam=None, info=None): 85 | cam_desc=cls.build_cam_desc(params={"name":info.name}) 86 | cam_desc["display_name"]="{} {}".format(info.vendor,info.model) 87 | cam_name="basler_pylon_{}".format(idx) 88 | return cam_name,cam_desc 89 | 90 | def get_kind_name(self): 91 | return "Generic Basler pylon" 92 | def make_thread(self, name): 93 | return BaslerPylonCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 94 | 95 | def make_gui_control(self, parent): 96 | return Settings_GUI(parent,cam_desc=self) 97 | def make_gui_status(self, parent): 98 | return GenericCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/Bonito.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import IMAQ 2 | from pylablib.devices.AlliedVision import Bonito 3 | from pylablib.thread.devices.AlliedVision import IMAQBonitoCameraThread 4 | from pylablib.core.thread import controller 5 | 6 | from .base import ICameraDescriptor 7 | from ..gui import cam_gui_parameters 8 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 9 | 10 | 11 | 12 | 13 | 14 | class BlackOffsetParameter(cam_gui_parameters.IGUIParameter): 15 | def add(self, base): 16 | self.base=base 17 | self.base.add_check_box("change_bl_offset","Change black level offset",False,add_indicator=False) 18 | self.base.add_num_edit("bl_offset",value=0,limiter=(0,None,"coerce","int"),formatter="int",label="Black level offset") 19 | self.base.vs["change_bl_offset"].connect(controller.exsafe(lambda v: self.base.set_enabled("bl_offset",v))) 20 | self.base.set_enabled("bl_offset",False) 21 | self.connect_updater(["bl_offset","change_bl_offset"]) 22 | def _update_value(self, v): 23 | if self.base.v["change_bl_offset"]: 24 | super()._update_value(v) 25 | def collect(self, parameters): 26 | if self.base.v["change_bl_offset"]: 27 | parameters["bl_offset"]=self.base.v["bl_offset"] 28 | return super().collect(parameters) 29 | def display(self, parameters): 30 | if "bl_offset" in parameters: 31 | self.base.i["bl_offset"]=parameters["bl_offset"] 32 | return super().display(parameters) 33 | 34 | 35 | class BonitoCameraSettings_GUI(GenericCameraSettings_GUI): 36 | _roi_kind="minsize" 37 | def get_basic_parameters(self, name): 38 | """Get basic GUI parameters, which can be shared between different cameras""" 39 | if name=="exposure": return cam_gui_parameters.FloatGUIParameter(self,"exposure","Exposure (ms)",limit=(0,None),fmt=".4f",default=100,factor=1E3) 40 | if name=="frame_period": return cam_gui_parameters.FloatGUIParameter(self,"frame_period","Frame period (ms)",limit=(0,None),fmt=".4f",default=0,factor=1E3) 41 | if name=="roi": return cam_gui_parameters.ROIGUIParameter(self,bin_kind=self._bin_kind,roi_kind=self._roi_kind) 42 | if name=="bl_offset": return BlackOffsetParameter(self) 43 | if name=="status_line": return cam_gui_parameters.BoolGUIParameter(self,"status_line","Status line",default=True) 44 | if name=="perform_status_check": return cam_gui_parameters.BoolGUIParameter(self,"perform_status_check","Perform status line check",default=True,add_indicator=False,indirect=True) 45 | return super().get_basic_parameters(name) 46 | def setup_settings_tables(self): 47 | super().setup_settings_tables() 48 | self.add_builtin_parameter("bl_offset","advanced") 49 | self.add_builtin_parameter("status_line","advanced").allow_diff_update=True 50 | self.add_builtin_parameter("perform_status_check","advanced").allow_diff_update=True 51 | self.advanced_params.vs["status_line"].connect(controller.exsafe(lambda v: self.advanced_params.set_enabled("perform_status_check",v))) 52 | def collect_parameters(self): 53 | parameters=super().collect_parameters() 54 | parameters["perform_status_check"]&=parameters["status_line"] 55 | return parameters 56 | 57 | 58 | 59 | class BonitoIMAQCameraDescriptor(ICameraDescriptor): 60 | _cam_kind="BonitoIMAQ" 61 | @classmethod 62 | def iterate_cameras(cls, verbose=False): 63 | if verbose: print("Searching for IMAQ Bonito cameras") 64 | try: 65 | imaq_cams=IMAQ.list_cameras() 66 | except (IMAQ.IMAQError, OSError): 67 | if verbose: print("Error loading or running the IMAQ library: required software (NI Vision) must be missing\n") 68 | if verbose=="full": cls.print_error() 69 | return 70 | cam_num=len(imaq_cams) 71 | if not cam_num: 72 | if verbose: print("Found no IMAQ cameras\n") 73 | return 74 | if verbose: print("Found {} IMAQ camera{}".format(cam_num,"s" if cam_num>1 else "")) 75 | for name in imaq_cams: 76 | try: 77 | if verbose: print("Found IMAQ camera {}".format(name)) 78 | with IMAQ.IMAQCamera(name) as cam: 79 | if not Bonito.check_grabber_association(cam): 80 | yield None,None 81 | continue 82 | yield None,name 83 | except IMAQ.IMAQError: 84 | if verbose=="full": cls.print_error() 85 | @classmethod 86 | def generate_description(cls, idx, cam=None, info=None): 87 | if info: 88 | imaq_name=info 89 | with Bonito.BonitoIMAQCamera(imaq_name=imaq_name) as cam: 90 | device_info=cam.get_device_info() 91 | cam_desc=cls.build_cam_desc(params={"imaq_name":imaq_name}) 92 | cam_desc["display_name"]=device_info.version.splitlines()[0] 93 | cam_name="allvis_bonito_imaq_{}".format(idx) 94 | return cam_name,cam_desc 95 | def get_kind_name(self): 96 | return "Bonito + IMAQ" 97 | def make_thread(self, name): 98 | return IMAQBonitoCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 99 | def make_gui_control(self, parent): 100 | return BonitoCameraSettings_GUI(parent,cam_desc=self) 101 | def make_gui_status(self, parent): 102 | return GenericCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/DCAM.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import DCAM 2 | from pylablib.thread.devices.DCAM import DCAMCameraThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui import cam_gui_parameters, cam_attributes_browser 6 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 7 | 8 | 9 | 10 | 11 | class DCAMOrcaCameraThread(DCAMCameraThread): 12 | parameter_variables=DCAMCameraThread.parameter_variables|{"defect_correct_mode"} 13 | 14 | class DCAMImagEMCameraThread(DCAMCameraThread): 15 | def _apply_additional_parameters(self, parameters): 16 | super()._apply_additional_parameters(parameters) 17 | if "sensitivity" in parameters: 18 | self.device.cav["SENSITIVITY"]=parameters["sensitivity"] 19 | def _update_additional_parameters(self, parameters): 20 | parameters["sensitivity"]=self.device.cav["SENSITIVITY"] 21 | return super()._update_additional_parameters(parameters) 22 | 23 | 24 | class CamAttributesBrowser(cam_attributes_browser.CamAttributesBrowser): 25 | def _add_attribute(self, name, attribute, value): 26 | indicator=not attribute.writable 27 | if attribute.kind=="int": 28 | self._record_attribute(name,"int",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 29 | self.add_integer_parameter(name,attribute.name,limits=(attribute.min,attribute.max),default=attribute.default,indicator=indicator) 30 | elif attribute.kind=="float": 31 | self._record_attribute(name,"float",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 32 | self.add_float_parameter(name,attribute.name,limits=(attribute.min,attribute.max),default=attribute.default,indicator=indicator) 33 | elif attribute.kind=="enum": 34 | self._record_attribute(name,"enum",attribute,indicator=indicator,rng=attribute.ilabels) 35 | self.add_choice_parameter(name,attribute.name,attribute.ilabels,indicator=indicator) 36 | def _get_attribute_range(self, attribute): 37 | if attribute.kind in ["int","float"]: 38 | return (attribute.min,attribute.max) 39 | if attribute.kind=="enum": 40 | return attribute.ilabels 41 | 42 | 43 | 44 | class Settings_GUI(GenericCameraSettings_GUI): 45 | _bin_kind="same" 46 | _frame_period_kind="indicator" 47 | def setup_settings_tables(self): 48 | super().setup_settings_tables() 49 | self.add_parameter(cam_gui_parameters.EnumGUIParameter(self,"readout_speed","Readout speed",{"slow":"Slow","normal":"Normal","fast":"Fast"}),"advanced") 50 | self.add_parameter(cam_gui_parameters.AttributesBrowserGUIParameter(self,CamAttributesBrowser),"advanced") 51 | 52 | class OrcaSettings_GUI(Settings_GUI): 53 | def setup_settings_tables(self): 54 | super().setup_settings_tables() 55 | self.add_parameter(cam_gui_parameters.BoolGUIParameter(self,"defect_correct_mode","Defect correction",default=True),"advanced",row=-1) 56 | 57 | class ImagEMSettings_GUI(Settings_GUI): 58 | def setup_settings_tables(self): 59 | super().setup_settings_tables() 60 | self.add_parameter(cam_gui_parameters.IntGUIParameter(self,"sensitivity","EMCCD sensitivity",(0,255)),"advanced",row=-1) 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | class DCAMCameraDescriptor(ICameraDescriptor): 69 | _cam_kind="DCAM" 70 | 71 | @classmethod 72 | def iterate_cameras(cls, verbose=False): 73 | if verbose: print("Searching for DCAM cameras") 74 | try: 75 | cam_num=DCAM.get_cameras_number() 76 | except (DCAM.DCAMError, OSError): 77 | if verbose: print("Error loading or running the DCAM library: required software (Hamamtsu HOKAWO or DCAM API) must be missing\n") 78 | if verbose=="full": cls.print_error() 79 | return 80 | if cam_num==0: 81 | if verbose: print("Found no DCAM cameras\n") 82 | return 83 | if verbose: print("Found {} DCAM camera{}".format(cam_num,"s" if cam_num>1 else "")) 84 | for i in range(cam_num): 85 | try: 86 | if verbose: print("Found DCAM camera idx={}".format(i)) 87 | with DCAM.DCAMCamera(idx=i) as cam: 88 | device_info=cam.get_device_info() 89 | if verbose: print("\tVendor {}, model {}".format(device_info.vendor,device_info.model)) 90 | yield cam,None 91 | except DCAM.DCAMError: 92 | if verbose=="full": cls.print_error() 93 | @classmethod 94 | def generate_description(cls, idx, cam=None, info=None): 95 | device_info=cam.get_device_info() 96 | cam_desc=cls.build_cam_desc(params={"idx":idx}) 97 | cam_desc["display_name"]="{} {}".format(device_info.model,device_info.serial_number) 98 | cam_name="dcam_{}".format(idx) 99 | return cam_name,cam_desc 100 | 101 | def get_kind_name(self): 102 | return "Generic Hamamatsu" 103 | 104 | def make_thread(self, name): 105 | return DCAMCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 106 | 107 | def make_gui_control(self, parent): 108 | return Settings_GUI(parent,cam_desc=self) 109 | def make_gui_status(self, parent): 110 | return GenericCameraStatus_GUI(parent,cam_desc=self) 111 | 112 | 113 | 114 | 115 | class DCAMOrcaCameraDescriptor(DCAMCameraDescriptor): 116 | _cam_kind="DCAMOrca" 117 | _expands="DCAM" 118 | @classmethod 119 | def generate_description(cls, idx, cam=None, info=None): 120 | if cam.get_device_info().model.lower().startswith("c11440"): 121 | return super().generate_description(idx,cam=cam,info=info) 122 | def get_kind_name(self): 123 | return "Hamamatsu Orca" 124 | def make_thread(self, name): 125 | return DCAMOrcaCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 126 | def make_gui_control(self, parent): 127 | return OrcaSettings_GUI(parent,cam_desc=self) 128 | 129 | 130 | class DCAMImagEMCameraDescriptor(DCAMCameraDescriptor): 131 | _cam_kind="DCAMImagEM" 132 | _expands="DCAM" 133 | @classmethod 134 | def generate_description(cls, idx, cam=None, info=None): 135 | if cam.get_device_info().model.lower().startswith("c9100"): 136 | return super().generate_description(idx,cam=cam,info=info) 137 | def get_kind_name(self): 138 | return "Hamamatsu ImagEM" 139 | def make_thread(self, name): 140 | return DCAMImagEMCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 141 | def make_gui_control(self, parent): 142 | return ImagEMSettings_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/IMAQdx.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import IMAQdx 2 | from pylablib.thread.devices.IMAQdx import IMAQdxCameraThread, EthernetIMAQdxCameraThread 3 | from pylablib.core.thread import controller 4 | 5 | from .base import ICameraDescriptor 6 | from ..gui import cam_gui_parameters, cam_attributes_browser 7 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 8 | 9 | 10 | 11 | 12 | class EthernetPhotonFocusIMAQdxCameraThread(EthernetIMAQdxCameraThread): 13 | parameter_variables=EthernetIMAQdxCameraThread.parameter_variables|{"exposure"} 14 | def _apply_additional_parameters(self, parameters): 15 | super()._apply_additional_parameters(parameters) 16 | if "exposure" in parameters: 17 | self.device.cav["CameraAttributes/AcquisitionControl/ExposureTime"]=parameters["exposure"]*1E6 18 | def _update_additional_parameters(self, parameters): 19 | parameters["exposure"]=self.device.cav["CameraAttributes/AcquisitionControl/ExposureTime"]/1E6 20 | return super()._update_additional_parameters(parameters) 21 | def _estimate_buffers_num(self): 22 | if self.device: 23 | nframes=self.min_buffer_size[1] 24 | if "CameraAttributes/AcquisitionControl/AcquisitionFrameRateMax" in self.device.cav: 25 | n_rate=self.min_buffer_size[0]*self.device.cav["CameraAttributes/AcquisitionControl/AcquisitionFrameRateMax"] 26 | nframes=max(nframes,n_rate) 27 | return int(nframes) 28 | return None 29 | 30 | 31 | class CamAttributesBrowser(cam_attributes_browser.CamAttributesBrowser): 32 | def setup(self, cam_ctl): 33 | super().setup(cam_ctl) 34 | with self.buttons.using_layout("buttons"): 35 | self.buttons.add_combo_box("visibility",label="Visibility",options={"simple":"Simple","intermediate":"Intermediate","advanced":"Advanced"},value="simple",location=(0,0)) 36 | self.buttons.vs["visibility"].connect(self.setup_visibility) 37 | def _add_attribute(self, name, attribute, value): 38 | if not attribute.readable: 39 | return 40 | indicator=not attribute.writable 41 | if attribute.kind in ["u32","i64"]: 42 | self._record_attribute(name,"int",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 43 | self.add_integer_parameter(name,attribute.name,limits=(attribute.min,attribute.max),indicator=indicator) 44 | elif attribute.kind=="f64": 45 | self._record_attribute(name,"float",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 46 | self.add_float_parameter(name,attribute.name,limits=(attribute.min,attribute.max),indicator=indicator) 47 | elif attribute.kind=="enum": 48 | self._record_attribute(name,"enum",attribute,indicator=indicator,rng=attribute.ilabels) 49 | self.add_choice_parameter(name,attribute.name,attribute.ilabels,indicator=indicator) 50 | elif attribute.kind=="str": 51 | self._record_attribute(name,"str",attribute,indicator=indicator) 52 | self.add_string_parameter(name,attribute.name,indicator=indicator) 53 | elif attribute.kind=="bool": 54 | self._record_attribute(name,"bool",attribute,indicator=indicator) 55 | self.add_bool_parameter(name,attribute.name,indicator=indicator) 56 | def _get_attribute_range(self, attribute): 57 | if attribute.kind in ["u32","i64","f64"]: 58 | return (attribute.min,attribute.max) 59 | if attribute.kind=="enum": 60 | return attribute.ilabels 61 | @controller.exsafe 62 | def setup_visibility(self): 63 | quick=self.buttons.v["quick_access"] 64 | vis=self.buttons.v["visibility"] 65 | vis_order=["simple","intermediate","advanced"] 66 | for n in self._attributes: 67 | vis_pass=vis_order.index(self._attributes[n].attribute.visibility)<=vis_order.index(vis) 68 | self._show_attribute(n,(not quick or self.props_table.v["p_quick",n]) and vis_pass) 69 | def setup_parameters(self, full_info): 70 | super().setup_parameters(full_info) 71 | self.setup_visibility() 72 | 73 | 74 | 75 | class Settings_GUI(GenericCameraSettings_GUI): 76 | def setup_settings_tables(self): 77 | super().setup_settings_tables() 78 | self.add_parameter(cam_gui_parameters.AttributesBrowserGUIParameter(self,CamAttributesBrowser),"advanced") 79 | 80 | 81 | 82 | class IMAQdxCameraDescriptor(ICameraDescriptor): 83 | _cam_kind="IMAQdx" 84 | 85 | @classmethod 86 | def iterate_cameras(cls, verbose=False): 87 | if verbose: print("Searching for IMAQdx cameras") 88 | try: 89 | cams=IMAQdx.list_cameras() 90 | except (IMAQdx.IMAQdxError, OSError, AttributeError): 91 | if verbose: print("Error loading or running the IMAQdx library: required software (NI IMAQdx) must be missing\n") 92 | if verbose=="full": cls.print_error() 93 | return 94 | if len(cams)==0: 95 | if verbose: print("Found no IMAQdx cameras\n") 96 | return 97 | cam_num=len(cams) 98 | if verbose: print("Found {} IMAQdx camera{}".format(cam_num,"s" if cam_num>1 else "")) 99 | for i,cdesc in enumerate(cams): 100 | if verbose: print("Checking IMAQdx camera idx={}\n\tVendor {}, model {}".format(i,cdesc.vendor,cdesc.model)) 101 | yield None,cdesc 102 | @classmethod 103 | def _generate_default_description(cls, idx, cam=None, info=None): 104 | cam_desc=cls.build_cam_desc(params={"name":info.name}) 105 | cam_desc["display_name"]="{} {}".format(info.vendor,info.model) 106 | cam_name="imaqdx_{}".format(idx) 107 | return cam_name,cam_desc 108 | 109 | @classmethod 110 | def generate_description(cls, idx, cam=None, info=None): 111 | return None,None 112 | def get_kind_name(self): 113 | return "Generic IMAQdx" 114 | def make_thread(self, name): 115 | return IMAQdxCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 116 | 117 | def make_gui_control(self, parent): 118 | return Settings_GUI(parent,cam_desc=self) 119 | def make_gui_status(self, parent): 120 | return GenericCameraStatus_GUI(parent,cam_desc=self) 121 | 122 | 123 | 124 | class EthernetPhotonFocusIMAQdxCameraDescriptor(IMAQdxCameraDescriptor): 125 | _cam_kind="PhotonFocusLAN" 126 | _expands="IMAQdx" 127 | @classmethod 128 | def generate_description(cls, idx, cam=None, info=None): 129 | if info.vendor.lower().startswith("photonfocus") and info.model.lower().startswith("hd1"): 130 | return super()._generate_default_description(idx,cam=cam,info=info) 131 | def get_kind_name(self): 132 | return "PhotonFocus Ethernet" 133 | def make_thread(self, name): 134 | return EthernetPhotonFocusIMAQdxCameraThread(name=name,kwargs=self.settings["params"].as_dict()) -------------------------------------------------------------------------------- /utils/cameras/PCOSC2.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import PCO 2 | from pylablib.thread.devices.PCO import PCOSC2CameraThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui import cam_gui_parameters 6 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 7 | 8 | 9 | 10 | 11 | 12 | 13 | class BasicPCOSC2CameraThread(PCOSC2CameraThread): 14 | def setup_open_device(self): 15 | super().setup_open_device() 16 | self.device.set_status_line_mode(True,False) 17 | self._status_line_enabled=True 18 | try: 19 | self.device.enable_pixel_correction() 20 | except self.device.Error: 21 | pass 22 | 23 | 24 | 25 | class FastScanBoolGUIParameter(cam_gui_parameters.BoolGUIParameter): 26 | """Fast scan parameter""" 27 | def __init__(self, settings): 28 | super().__init__(settings,"fast_scan","Fast scan",default=True,cam_name="pixel_rate") 29 | def to_camera(self, gui_value): 30 | return None if gui_value else 0 31 | def display(self, parameters): 32 | if "pixel_rate" in parameters and "all_pixel_rates" in parameters: 33 | self.settings.i[self.gui_name]=(parameters["pixel_rate"]==parameters["all_pixel_rates"][-1]) 34 | 35 | class Settings_GUI(GenericCameraSettings_GUI): 36 | _bin_kind="both" 37 | _frame_period_kind="value" 38 | def setup_settings_tables(self): 39 | super().setup_settings_tables() 40 | self.add_parameter(FastScanBoolGUIParameter(self),"advanced") 41 | perform_status_check=cam_gui_parameters.BoolGUIParameter(self,"perform_status_check","Perform status line check",default=True,add_indicator=False,indirect=True) 42 | self.add_parameter(perform_status_check,"advanced").allow_diff_update=True 43 | 44 | class Status_GUI(GenericCameraStatus_GUI): 45 | def setup_status_table(self): 46 | self.add_text_label("buffer_overruns",label="Buffer overruns:") 47 | # Update the interface indicators according to camera parameters 48 | def show_parameters(self, params): 49 | super().show_parameters(params) 50 | if "internal_buffer_status" in params: 51 | buffer_overruns=params["internal_buffer_status"].overruns 52 | self.v["buffer_overruns"]=str(buffer_overruns) if buffer_overruns is not None else "N/A" 53 | self.w["buffer_overruns"].setStyleSheet("font-weight: bold" if buffer_overruns else "") 54 | 55 | 56 | 57 | class PCOCameraDescriptor(ICameraDescriptor): 58 | _cam_kind="PCOSC2" 59 | 60 | @classmethod 61 | def iterate_cameras(cls, verbose=False): 62 | if verbose: print("Searching for PCO cameras") 63 | try: 64 | cam_num=PCO.get_cameras_number() 65 | except (PCO.PCOSC2Error, OSError): 66 | if verbose: print("Error loading or running the PCO SC2 library: required software (PCO SDK) must be missing\n") 67 | if verbose=="full": cls.print_error() 68 | return 69 | if cam_num==0: 70 | if verbose: print("Found no PCO cameras\n") 71 | return 72 | if verbose: print("Found {} PCO camera{}".format(cam_num,"s" if cam_num>1 else "")) 73 | for i in range(cam_num): 74 | try: 75 | if verbose: print("Found PCO camera idx={}".format(i)) 76 | with PCO.PCOSC2Camera(idx=i) as cam: 77 | device_info=cam.get_device_info() 78 | if verbose: print("\tModel {}, serial number {}".format(device_info.model,device_info.serial_number)) 79 | yield cam,None 80 | except PCO.PCOSC2Error: 81 | if verbose=="full": cls.print_error() 82 | @classmethod 83 | def generate_description(cls, idx, cam=None, info=None): 84 | device_info=cam.get_device_info() 85 | cam_desc=cls.build_cam_desc(params={"idx":idx}) 86 | cam_desc["display_name"]="{} {}".format(device_info.model,device_info.serial_number) 87 | cam_name="pcosc2_{}".format(idx) 88 | return cam_name,cam_desc 89 | 90 | def get_kind_name(self): 91 | return "Generic PCO" 92 | @classmethod 93 | def get_class_settings(cls): 94 | settings=super().get_class_settings() 95 | settings["allow_garbage_collection"]=False 96 | return settings 97 | 98 | def make_thread(self, name): 99 | return BasicPCOSC2CameraThread(name=name,kwargs=self.settings["params"].as_dict()) 100 | 101 | def make_gui_control(self, parent): 102 | return Settings_GUI(parent,cam_desc=self) 103 | def make_gui_status(self, parent): 104 | return Status_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/ThorlabsTLCam.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import Thorlabs 2 | from pylablib.thread.devices.Thorlabs import ThorlabsTLCameraThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 6 | 7 | 8 | 9 | 10 | 11 | class Settings_GUI(GenericCameraSettings_GUI): 12 | _bin_kind="both" 13 | 14 | 15 | 16 | 17 | class ThorlabsTLCamCameraDescriptor(ICameraDescriptor): 18 | _cam_kind="ThorlabsTLCam" 19 | 20 | @classmethod 21 | def iterate_cameras(cls, verbose=False): 22 | if verbose: print("Searching for Thorlabs TSI cameras") 23 | try: 24 | cam_infos=Thorlabs.list_cameras_tlcam() 25 | except (Thorlabs.ThorlabsTLCameraError, OSError): 26 | if verbose: print("Error loading or running the Thorlabs TSI library: required software (ThorCam) must be missing\n") 27 | if verbose=="full": cls.print_error() 28 | return 29 | cam_num=len(cam_infos) 30 | if not cam_num: 31 | if verbose: print("Found no Thorlabs TLCam cameras\n") 32 | return 33 | if verbose: print("Found {} Thorlabs TLCam camera{}".format(cam_num,"s" if cam_num>1 else "")) 34 | for serial in cam_infos: 35 | try: 36 | if verbose: print("Found Thorlabs TSI camera serial={}".format(serial)) 37 | with Thorlabs.ThorlabsTLCamera(serial) as cam: 38 | yield cam,serial 39 | except Thorlabs.ThorlabsTLCameraError: 40 | if verbose=="full": cls.print_error() 41 | @classmethod 42 | def generate_description(cls, idx, cam=None, info=None): 43 | device_info=cam.get_device_info() 44 | cam_desc=cls.build_cam_desc(params={"serial":info}) 45 | cam_desc["display_name"]="{} {}".format(device_info.model,device_info.serial_number) 46 | cam_name="thorlabs_tlcam_{}".format(idx) 47 | return cam_name,cam_desc 48 | 49 | def get_kind_name(self): 50 | return "Thorlabs Scientific Camera" 51 | 52 | def make_thread(self, name): 53 | return ThorlabsTLCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 54 | 55 | def make_gui_control(self, parent): 56 | return Settings_GUI(parent,cam_desc=self) 57 | def make_gui_status(self, parent): 58 | return GenericCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/__init__.py: -------------------------------------------------------------------------------- 1 | from .loader import find_camera_descriptors 2 | 3 | camera_descriptors=find_camera_descriptors() -------------------------------------------------------------------------------- /utils/cameras/base.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.utils import dictionary 2 | 3 | import traceback 4 | import sys 5 | 6 | 7 | class ICameraDescriptor: 8 | """ 9 | Base camera descriptor. 10 | 11 | Includes method to detect cameras of the given type, start threads, and create GUI (settings control and status display). 12 | """ 13 | _cam_kind=None 14 | _expands=None 15 | def __init__(self, name, settings=None): 16 | self.name=name 17 | self.settings=settings or {} 18 | 19 | @classmethod 20 | def print_added_camera(cls, name, desc): 21 | """Print information about a newly detected camera""" 22 | print("Adding camera under name {}:".format(name)) 23 | print("\tkind = '{}'".format(desc["kind"])) 24 | if "params" in desc: 25 | print("\tparams = '{}'".format(desc["params"].as_dict())) 26 | if "display_name" in desc: 27 | print("\tdisplay_name = '{}'".format(desc["display_name"])) 28 | print("") 29 | @classmethod 30 | def print_skipped_camera(cls): 31 | """Print information about a skipped camera""" 32 | print("Skipping the camera\n") 33 | @classmethod 34 | def print_error(cls): 35 | """Print an exception traceback""" 36 | traceback.print_exc() 37 | print("",file=sys.stderr) 38 | @classmethod 39 | def iterate_cameras(cls, verbose=False): 40 | """Iterate over all cameras of the given type""" 41 | raise NotImplementedError 42 | @classmethod 43 | def generate_description(cls, idx, cam=None, info=None): 44 | """ 45 | Return camera description dictionary for the given camera index, camera class, and additional info. 46 | 47 | Return either tuple ``(cam_name, cam_desc)`` or ``None`` (camera can not be added). 48 | """ 49 | 50 | @classmethod 51 | def build_cam_desc(cls, params, cam_kind=None): 52 | return dictionary.Dictionary({"kind":cam_kind or cls._cam_kind,"params":params or {}}) 53 | @classmethod 54 | def can_expand(cls, cam_kind): 55 | return cls._expands==cam_kind 56 | @classmethod 57 | def find_description(cls, idx, cam=None, info=None, camera_descriptors=None): 58 | """ 59 | Find the most specific description for the given camera index, camera class, and additional info. 60 | 61 | Return tuple ``(cls, desc)``, where ``desc`` is the result of :meth:`generate_description` call 62 | and ``cls`` is the class which generated this description. 63 | """ 64 | desc=cls.generate_description(idx,cam=cam,info=info) 65 | if desc and camera_descriptors: 66 | for d in camera_descriptors: 67 | if d.can_expand(cls._cam_kind): 68 | exp_cls,exp_desc=d.find_description(idx,cam=cam,info=info,camera_descriptors=camera_descriptors) 69 | if exp_desc is not None: 70 | return exp_cls,exp_desc 71 | return cls,desc 72 | @classmethod 73 | def detect(cls, verbose=False, camera_descriptors=None): 74 | """Detect all cameras of the given type""" 75 | cameras=dictionary.Dictionary() 76 | for i,(cam,info) in enumerate(cls.iterate_cameras(verbose=verbose)): 77 | desc_cls,desc=cls.find_description(i,cam=cam,info=info,camera_descriptors=camera_descriptors) 78 | if desc is not None and desc[0] is not None: 79 | if verbose: desc_cls.print_added_camera(*desc) 80 | cameras[desc[0]]=desc[1] 81 | else: 82 | if verbose: desc_cls.print_skipped_camera() 83 | return cameras 84 | def get_kind_name(self): 85 | """Get user-friendly name to be displayed in the GUI""" 86 | raise NotImplementedError 87 | def get_camera_labels(self): 88 | """Get label ``(kind_name, cam_name)``""" 89 | return self.get_kind_name(),self.settings.get("display_name",self.name) 90 | @classmethod 91 | def get_class_settings(cls): 92 | """Get dictionary with generic class settings""" 93 | return {"allow_garbage_collection":True} 94 | 95 | def make_thread(self, name): 96 | """Create camera thread with the given name""" 97 | raise NotImplementedError 98 | 99 | def make_gui_control(self, parent): 100 | """Create GUI settings control with the given parent widget""" 101 | raise NotImplementedError 102 | def make_gui_status(self, parent): 103 | """Create GUI status table with the given parent widget""" 104 | raise NotImplementedError -------------------------------------------------------------------------------- /utils/cameras/loader.py: -------------------------------------------------------------------------------- 1 | from .base import ICameraDescriptor 2 | 3 | from pylablib.core.utils import files as file_utils, string as string_utils 4 | 5 | import os 6 | import sys 7 | import importlib 8 | 9 | folder=os.path.dirname(__file__) 10 | root_module_name=__name__.rsplit(".",maxsplit=1)[0] 11 | def find_camera_descriptors(): 12 | """Find all camera descriptor classes""" 13 | files=file_utils.list_dir_recursive(folder,file_filter=r".*\.py$",visit_folder_filter=string_utils.get_string_filter(exclude="__pycache__")).files 14 | cam_classes={} 15 | for f in files: 16 | if f not in ["__init__.py","base.py"]: 17 | module_name="{}.{}".format(root_module_name,os.path.splitext(f)[0].replace("\\",".").replace("/",".")) 18 | if module_name not in sys.modules: 19 | spec=importlib.util.spec_from_file_location(module_name,os.path.join(folder,f)) 20 | mod=importlib.util.module_from_spec(spec) 21 | spec.loader.exec_module(mod) 22 | sys.modules[module_name]=mod 23 | mod=sys.modules[module_name] 24 | for v in mod.__dict__.values(): 25 | if isinstance(v,type) and issubclass(v,ICameraDescriptor) and v is not ICameraDescriptor and v._cam_kind is not None: 26 | cam_classes[v._cam_kind]=v 27 | return cam_classes -------------------------------------------------------------------------------- /utils/cameras/picam.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import PrincetonInstruments 2 | from pylablib.thread.devices.PrincetonInstruments import PicamCameraThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui import cam_gui_parameters, cam_attributes_browser 6 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 7 | 8 | 9 | 10 | 11 | 12 | class CamAttributesBrowser(cam_attributes_browser.CamAttributesBrowser): 13 | def _add_attribute(self, name, attribute, value): 14 | indicator=not attribute.writable 15 | if attribute.kind in {"Integer","Large Integer"}: 16 | self._record_attribute(name,"int",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 17 | self.add_integer_parameter(name,attribute.name,limits=(attribute.min,attribute.max),default=attribute.default,indicator=indicator) 18 | elif attribute.kind=="Floating Point": 19 | self._record_attribute(name,"float",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 20 | self.add_float_parameter(name,attribute.name,limits=(attribute.min,attribute.max),default=attribute.default,indicator=indicator) 21 | elif attribute.kind=="Enumeration": 22 | self._record_attribute(name,"enum",attribute,indicator=indicator,rng=attribute.ilabels) 23 | self.add_choice_parameter(name,attribute.name,attribute.ilabels,default=attribute.default,indicator=indicator) 24 | elif attribute.kind=="Boolean": 25 | self._record_attribute(name,"bool",attribute,indicator=indicator) 26 | self.add_bool_parameter(name,attribute.name,default=attribute.default,indicator=indicator) 27 | def _get_attribute_range(self, attribute): 28 | if attribute.kind in ["Integer","Large Integer","Floating Point"]: 29 | return (attribute.min,attribute.max) 30 | if attribute.kind=="Enumeration": 31 | return attribute.ilabels 32 | 33 | class Settings_GUI(GenericCameraSettings_GUI): 34 | _bin_kind="both" 35 | _frame_period_kind="indicator" 36 | def setup_settings_tables(self): 37 | super().setup_settings_tables() 38 | self.add_parameter(cam_gui_parameters.AttributesBrowserGUIParameter(self,CamAttributesBrowser),"advanced") 39 | 40 | 41 | 42 | 43 | class PicamCameraDescriptor(ICameraDescriptor): 44 | _cam_kind="Picam" 45 | 46 | @classmethod 47 | def iterate_cameras(cls, verbose=False): 48 | if verbose: print("Searching for Picam cameras") 49 | try: 50 | cams=PrincetonInstruments.list_cameras() 51 | except (PrincetonInstruments.PicamError, OSError): 52 | if verbose: print("Error loading or running the Picam library: required software (Princeton Instruments PICam) must be missing\n") 53 | if verbose=="full": cls.print_error() 54 | return 55 | if len(cams)==0: 56 | if verbose: print("Found no Picam cameras\n") 57 | return 58 | cam_num=len(cams) 59 | if verbose: print("Found {} Picam camera{}".format(cam_num,"s" if cam_num>1 else "")) 60 | for i,cdesc in enumerate(cams): 61 | if verbose: print("Found Picam camera serial number={}\n\tModel {}, name {}".format(i,cdesc.model,cdesc.name)) 62 | yield None,cdesc 63 | @classmethod 64 | def generate_description(cls, idx, cam=None, info=None): 65 | cam_desc=cls.build_cam_desc(params={"serial_number":info.serial_number}) 66 | cam_desc["display_name"]="{} {}".format(info.model,info.serial_number) 67 | cam_name="picam_{}".format(idx) 68 | return cam_name,cam_desc 69 | 70 | def get_kind_name(self): 71 | return "Princeton Instruments PICam" 72 | 73 | def make_thread(self, name): 74 | return PicamCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 75 | 76 | def make_gui_control(self, parent): 77 | return Settings_GUI(parent,cam_desc=self) 78 | def make_gui_status(self, parent): 79 | return GenericCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/pvcam.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import Photometrics 2 | from pylablib.thread.devices.Photometrics import PvcamCameraThread 3 | from pylablib.core.utils import dictionary 4 | 5 | from .base import ICameraDescriptor 6 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 7 | from ..gui import cam_gui_parameters, cam_attributes_browser 8 | 9 | 10 | 11 | class TriggerModeParameter(cam_gui_parameters.IGUIParameter): 12 | """ 13 | PVCam trigger mode parameter. 14 | 15 | Receives possible values from the camera. 16 | """ 17 | def __init__(self, settings): 18 | super().__init__(settings) 19 | self.disabled=False 20 | def add(self, base): 21 | self.base=base 22 | base.add_combo_box("trigger_mode",label="Trigger mode",options=["Internal"],index_values=[None],location={"indicator":"next_line"}) 23 | base.add_combo_box("trigger_out_mode",label="Trigger out",options=["None"],index_values=[None],location={"indicator":"next_line"}) 24 | self.connect_updater(["trigger_mode","trigger_out_mode"]) 25 | def setup(self, parameters, full_info): 26 | super().setup(parameters,full_info) 27 | if "parameter_ranges/trigger_mode" in full_info: 28 | trig_modes,trig_out_modes=full_info["parameter_ranges/trigger_mode"] 29 | index_values=list(trig_modes) 30 | self.base.w["trigger_mode"].set_options([trig_modes[v] for v in index_values],index_values=index_values,value=index_values[0]) 31 | index_values=list(trig_out_modes) 32 | self.base.w["trigger_out_mode"].set_options([trig_out_modes[v] for v in index_values],index_values=index_values,value=index_values[0]) 33 | if index_values==[None]: 34 | self.base.set_enabled("trigger_out_mode",False) 35 | else: 36 | self.disabled=True 37 | self.base.set_enabled(["trigger_mode","trigger_out_mode"],False) 38 | def collect(self, parameters): 39 | if not self.disabled: 40 | parameters["trigger_mode"]=self.base.v["trigger_mode"],self.base.v["trigger_out_mode"] 41 | return super().collect(parameters) 42 | def display(self, parameters): 43 | if "trigger_mode" in parameters and not self.disabled: 44 | self.base.i["trigger_mode"],self.base.i["trigger_out_mode"]=parameters["trigger_mode"] 45 | return super().display(parameters) 46 | 47 | class ClearModeParameter(cam_gui_parameters.EnumGUIParameter): 48 | """ 49 | PVCam clear mode parameter. 50 | 51 | Receives possible values from the camera. 52 | """ 53 | def __init__(self, settings): 54 | super().__init__(settings,"clear_mode","Clear mode",{None:"None"}) 55 | self.add_indicator="next_line" 56 | def setup(self, parameters, full_info): 57 | super().setup(parameters,full_info) 58 | if "parameter_ranges/clear_mode" in full_info: 59 | self.base.w[self.gui_name].set_options(dictionary.as_dict(full_info["parameter_ranges/clear_mode"]),index=0) 60 | else: 61 | self.disable() 62 | 63 | class ClearCyclesParameter(cam_gui_parameters.IntGUIParameter): 64 | """ 65 | PVCam clear cycles parameter. 66 | 67 | Gets disabled if not supported by the camera. 68 | """ 69 | def __init__(self, settings): 70 | super().__init__(settings,"clear_cycles","Clear cycles",limit=(0,None)) 71 | def setup(self, parameters, full_info): 72 | super().setup(parameters,full_info) 73 | if "camera_attributes/CLEAR_CYCLES" not in full_info: 74 | self.disable() 75 | 76 | class ReadoutModeParameter(cam_gui_parameters.EnumGUIParameter): 77 | """ 78 | PVCam readout mode parameter. 79 | 80 | Receives possible values from the camera. 81 | """ 82 | def __init__(self, settings): 83 | super().__init__(settings,"readout_mode","Mode",{(0,0,0):"Default"}) 84 | self.add_indicator="next_line" 85 | def add(self, base): 86 | with base.using_new_sublayout("readout_mode","grid"): 87 | return super().add(base) 88 | def setup(self, parameters, full_info): 89 | super().setup(parameters,full_info) 90 | if "readout_modes" in full_info: 91 | readout_modes=full_info["readout_modes"] 92 | options=["{}, {:.0f}MHz, {}".format(m.port_name,m.speed_freq/1E6,m.gain_name) for m in readout_modes] 93 | index_values=[(m.port_idx,m.speed_idx,m.gain_idx) for m in readout_modes] 94 | self.base.w[self.gui_name].set_options(options,index_values=index_values,value=index_values[0]) 95 | else: 96 | self.disable() 97 | 98 | 99 | class CamAttributesBrowser(cam_attributes_browser.CamAttributesBrowser): 100 | def _add_attribute(self, name, attribute, value): 101 | if not attribute.readable: 102 | return 103 | indicator=not attribute.writable 104 | if attribute.kind in {"INT8","INT16","INT32","INT64","UNS8","UNS16","UNS32","UNS64"}: 105 | self._record_attribute(name,"int",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 106 | self.add_integer_parameter(name,attribute.name,limits=(attribute.min,attribute.max),indicator=indicator) 107 | elif attribute.kind in {"FLT32","FLT64"}: 108 | self._record_attribute(name,"float",attribute,indicator=indicator,rng=(attribute.min,attribute.max)) 109 | self.add_float_parameter(name,attribute.name,limits=(attribute.min,attribute.max),indicator=indicator) 110 | elif attribute.kind=="ENUM": 111 | self._record_attribute(name,"enum",attribute,indicator=indicator,rng=attribute.ilabels) 112 | self.add_choice_parameter(name,attribute.name,attribute.ilabels,indicator=indicator) 113 | elif attribute.kind=="CHAR_PTR": 114 | self._record_attribute(name,"str",attribute,indicator=indicator) 115 | self.add_string_parameter(name,attribute.name,indicator=indicator) 116 | elif attribute.kind=="BOOLEAN": 117 | self._record_attribute(name,"bool",attribute,indicator=indicator) 118 | self.add_bool_parameter(name,attribute.name,indicator=indicator) 119 | def _get_attribute_range(self, attribute): 120 | if attribute.kind in {"INT8","INT16","INT32","INT64","UNS8","UNS16","UNS32","UNS64","FLT32","FLT64"}: 121 | return (attribute.min,attribute.max) 122 | if attribute.kind=="ENUM": 123 | return attribute.ilabels 124 | 125 | class Settings_GUI(GenericCameraSettings_GUI): 126 | _bin_kind="both" 127 | _frame_period_kind="indicator" 128 | def get_basic_parameters(self, name): 129 | if name=="trigger_mode": return TriggerModeParameter(self) 130 | if name=="exposure": return cam_gui_parameters.FloatGUIParameter(self,"exposure","Exposure (ms)",limit=(0,None),fmt=".4f",default=100,factor=1E3) 131 | return super().get_basic_parameters(name) 132 | def setup_settings_tables(self): 133 | super().setup_settings_tables() 134 | self.add_parameter(ReadoutModeParameter(self),"advanced") 135 | self.add_parameter(ClearModeParameter(self),"advanced") 136 | self.add_parameter(ClearCyclesParameter(self),"advanced") 137 | self.add_parameter(cam_gui_parameters.AttributesBrowserGUIParameter(self,CamAttributesBrowser),"advanced") 138 | 139 | 140 | 141 | 142 | class PvcamCameraDescriptor(ICameraDescriptor): 143 | _cam_kind="pvcam" 144 | 145 | @classmethod 146 | def iterate_cameras(cls, verbose=False): 147 | if verbose: print("Searching for Pvcam cameras") 148 | try: 149 | cams=Photometrics.list_cameras() 150 | except (Photometrics.PvcamError, OSError): 151 | if verbose: print("Error loading or running the Pvcam library: required software (Photometrics PVCAM) must be missing\n") 152 | if verbose=="full": cls.print_error() 153 | return 154 | cam_num=len(cams) 155 | if not cam_num: 156 | if verbose: print("Found no Pvcam cameras\n") 157 | return 158 | if verbose: print("Found {} Pvcam camera{}".format(cam_num,"s" if cam_num>1 else "")) 159 | for name in cams: 160 | try: 161 | with Photometrics.PvcamCamera(name) as cam: 162 | device_info=cam.get_device_info() 163 | if verbose: print("Found Pvcam camera name={}, product {}, serial {}".format(name,device_info.product,device_info.serial)) 164 | yield cam,name 165 | except Photometrics.PvcamError: 166 | if verbose: print("Could not open Pvcam camera name={}".format(name)) 167 | if verbose=="full": cls.print_error() 168 | @classmethod 169 | def generate_description(cls, idx, cam=None, info=None): 170 | device_info=cam.get_device_info() 171 | cam_desc=cls.build_cam_desc(params={"cam_name":info}) 172 | cam_desc["display_name"]=" ".join(s for s in [device_info.vendor,device_info.system,device_info.serial] if s) 173 | cam_name="pvcam_{}".format(idx) 174 | return cam_name,cam_desc 175 | 176 | def get_kind_name(self): 177 | return "Photometrics PVCAM" 178 | 179 | def make_thread(self, name): 180 | return PvcamCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 181 | 182 | def make_gui_control(self, parent): 183 | return Settings_GUI(parent,cam_desc=self) 184 | def make_gui_status(self, parent): 185 | return GenericCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/sim.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices.interface import camera 2 | from pylablib.thread.devices.generic.camera import GenericCameraThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 6 | 7 | import time 8 | import collections 9 | import numpy as np 10 | 11 | 12 | TDeviceInfo=collections.namedtuple("TDeviceInfo",["kind"]) 13 | class SimulatedCamera(camera.IROICamera, camera.IExposureCamera): 14 | """ 15 | Generic simulated camera. 16 | 17 | Allows settings exposure (equal to frame rate), ROI, and frame buffer. 18 | 19 | Args: 20 | size: full "sensor" size 21 | """ 22 | def __init__(self, size=(1024,1024)): 23 | super().__init__() 24 | self._size=size 25 | self._roi=(0,size[0],0,size[1]) 26 | self._exposure=.1 27 | self._opened=False 28 | self._acquistion_started=None 29 | self.open() 30 | self._add_info_variable("device_info",self.get_device_info) 31 | 32 | def open(self): 33 | self._opened=True 34 | def close(self): 35 | if self._opened: 36 | self.clear_acquisition() 37 | self._opened=False 38 | def is_opened(self): 39 | return self._opened 40 | def _get_connection_parameters(self): 41 | return (self._size,) 42 | 43 | def get_device_info(self): 44 | return TDeviceInfo("simulated_basic",) 45 | 46 | ### Generic controls ### 47 | _min_exposure=1E-3 48 | def get_frame_timings(self): 49 | return self._TAcqTimings(self._exposure,self._exposure) 50 | @camera.acqstopped 51 | def set_exposure(self, exposure): 52 | self._exposure=max(exposure,self._min_exposure) 53 | return self._exposure 54 | 55 | ### Acquisition process controls ### 56 | def setup_acquisition(self, nframes=100): # pylint: disable=arguments-differ 57 | super().setup_acquisition(nframes=nframes) 58 | def clear_acquisition(self): 59 | self.stop_acquisition() 60 | super().clear_acquisition() 61 | def start_acquisition(self, *args, **kwargs): 62 | self.stop_acquisition() 63 | super().start_acquisition(*args,**kwargs) 64 | self._acquistion_started=time.time() 65 | self._frame_counter.reset(self._acq_params["nframes"]) 66 | def stop_acquisition(self): 67 | if self.acquisition_in_progress(): 68 | self._frame_counter.update_acquired_frames(self._get_acquired_frames()) 69 | self._acquistion_started=None 70 | def acquisition_in_progress(self): 71 | return self._acquistion_started is not None 72 | def get_frames_status(self): 73 | if self.acquisition_in_progress(): 74 | self._frame_counter.update_acquired_frames(self._get_acquired_frames()) 75 | return self._TFramesStatus(*self._frame_counter.get_frames_status()) 76 | def _get_acquired_frames(self): 77 | if self._acquistion_started is None: 78 | return None 79 | return int((time.time()-self._acquistion_started)//self._exposure) 80 | 81 | ### Image settings and transfer controls ### 82 | def get_detector_size(self): 83 | return self._size 84 | def get_roi(self): 85 | return self._roi 86 | @camera.acqcleared 87 | def set_roi(self, hstart=0, hend=None, vstart=0, vend=None): 88 | hlim,vlim=self.get_roi_limits() 89 | hstart,hend=self._truncate_roi_axis((hstart,hend),hlim) 90 | vstart,vend=self._truncate_roi_axis((vstart,vend),vlim) 91 | self._roi=(hstart,hend,vstart,vend) 92 | return self.get_roi() 93 | def get_roi_limits(self, hbin=1, vbin=1): 94 | wdet,hdet=self.get_detector_size() 95 | hlim=camera.TAxisROILimit(1,wdet,1,1,1) 96 | vlim=camera.TAxisROILimit(1,hdet,1,1,1) 97 | return hlim,vlim 98 | 99 | def _get_data_dimensions_rc(self): 100 | roi=self.get_roi() 101 | return (roi[3]-roi[2]),(roi[1]-roi[0]) 102 | _support_chunks=True 103 | def _get_base_frame(self): 104 | """Generate the base static noise-free frame""" 105 | xs,ys=np.meshgrid(np.arange(self._size[0]),np.arange(self._size[1]),indexing="ij") 106 | ip,jp=self._size[0]/2,self._size[1]/2 107 | iw,jw=self._size[0]/10,self._size[0]/20 108 | mag=1024 109 | return np.exp(-(xs-ip)**2/(2*iw**2)-(ys-jp)**2/(2*jw**2))*mag 110 | def _read_frames(self, rng, return_info=False): 111 | c0,c1,r0,r1=self._roi 112 | base=self._get_base_frame()[r0:r1,c0:c1].astype(self._default_image_dtype) 113 | return [base+np.random.randint(0,256,size=(rng[1]-rng[0],)+base.shape,dtype=self._default_image_dtype)],None 114 | 115 | 116 | 117 | class SimulatedCameraThread(GenericCameraThread): 118 | """Device thread for a simulated camera""" 119 | parameter_variables=GenericCameraThread.parameter_variables|{"exposure","frame_period","detector_size","buffer_size","acq_status","roi_limits","roi"} 120 | def connect_device(self): 121 | self.device=SimulatedCamera(size=self.cam_size) 122 | def setup_task(self, size=(1024,1024), remote=None, misc=None): # pylint: disable=arguments-differ 123 | self.cam_size=size 124 | super().setup_task(remote=remote,misc=misc) 125 | 126 | 127 | 128 | 129 | class SimulatedCameraDescriptor(ICameraDescriptor): 130 | _cam_kind="simulated" 131 | 132 | @classmethod 133 | def iterate_cameras(cls, verbose=False): 134 | yield from [] # do not discover by default 135 | 136 | def get_kind_name(self): 137 | return "Simulated camera" 138 | 139 | def make_thread(self, name): 140 | return SimulatedCameraThread(name=name,kwargs=self.settings["params"].as_dict()) 141 | 142 | def make_gui_control(self, parent): 143 | return GenericCameraSettings_GUI(parent,cam_desc=self) 144 | def make_gui_status(self, parent): 145 | return GenericCameraStatus_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/cameras/uc480.py: -------------------------------------------------------------------------------- 1 | from pylablib.devices import uc480 2 | from pylablib.thread.devices.uc480 import UC480CameraThread 3 | 4 | from .base import ICameraDescriptor 5 | from ..gui.base_cam_ctl_gui import GenericCameraSettings_GUI, GenericCameraStatus_GUI 6 | from ..gui import cam_gui_parameters 7 | 8 | 9 | 10 | class PixelRateFloatGUIParameter(cam_gui_parameters.FloatGUIParameter): 11 | """ 12 | Pixel rate parameter. 13 | 14 | Same as the basic floating point parameter, but automatically updates limits upon setup. 15 | """ 16 | def __init__(self, settings, indicator=False, cam_name=None, cam_range_name=None): 17 | super().__init__(settings,"pixel_rate","Pixel rate (MHz)",limit=(0,None),indicator=indicator,factor=1E-6,cam_name=cam_name) 18 | self.cam_range_name=cam_range_name 19 | def setup(self, parameters, full_info): 20 | super().setup(parameters,full_info) 21 | if self.cam_range_name is not None and self.cam_range_name in full_info: 22 | rmin,rmax=full_info[self.cam_range_name][:2] 23 | self.base.w[self.gui_name].set_limiter((rmin*self.factor,rmax*self.factor)) 24 | 25 | 26 | class GainFloatGUIParameter(cam_gui_parameters.FloatGUIParameter): 27 | """ 28 | Gain rate parameter. 29 | 30 | Same as the basic floating point parameter, but automatically updates limits upon setup. 31 | """ 32 | def __init__(self, settings, indicator=False): 33 | super().__init__(settings,"master_gain","Gain",limit=(0,None),indicator=indicator,cam_name="gains",to_camera=lambda v: (v,None,None,None),from_camera=lambda v: v[0]) 34 | def setup(self, parameters, full_info): 35 | super().setup(parameters,full_info) 36 | if "max_gains" in full_info: 37 | self.base.w[self.gui_name].set_limiter((1,full_info["max_gains"][0])) 38 | 39 | 40 | 41 | class Settings_GUI(GenericCameraSettings_GUI): 42 | _bin_kind="both" 43 | _frame_period_kind="value" 44 | def get_basic_parameters(self, name): 45 | if name=="pixel_rate": return PixelRateFloatGUIParameter(self,cam_range_name="pixel_rates_range") 46 | if name=="gain": return GainFloatGUIParameter(self) 47 | return super().get_basic_parameters(name) 48 | def setup_settings_tables(self): 49 | super().setup_settings_tables() 50 | self.add_builtin_parameter("pixel_rate","advanced") 51 | self.add_builtin_parameter("gain","advanced") 52 | 53 | class Status_GUI(GenericCameraStatus_GUI): 54 | def setup_status_table(self): 55 | self.add_num_label("frames_lost",formatter=("int"),label="Frames lost:") 56 | def show_parameters(self, params): 57 | super().show_parameters(params) 58 | if "acq_status" in params: 59 | self.v["frames_lost"]=params["acq_status"].transfer_missed 60 | self.w["frames_lost"].setStyleSheet("font-weight: bold" if params["acq_status"].transfer_missed else "") 61 | 62 | 63 | 64 | 65 | class UC480CameraDescriptor(ICameraDescriptor): 66 | _cam_kind="UC480" 67 | _backend_names={"uc480":"Throlabs uc480","ueye":"IDS uEye"} 68 | _backend_software={"uc480":"ThorCam","ueye":"IDS uEye"} 69 | 70 | @classmethod 71 | def _iterate_backend(cls, backend, verbose=False): 72 | if verbose: print("Searching for {} cameras".format(cls._backend_names[backend])) 73 | try: 74 | cam_infos=uc480.list_cameras(backend=backend) 75 | except (uc480.uc480Error, OSError): 76 | if verbose: print("Error loading or running {} library: required software ({}) must be missing\n".format( 77 | backend,cls._backend_software[backend])) 78 | if verbose=="full": cls.print_error() 79 | return 80 | cam_num=len(cam_infos) 81 | if not cam_num: 82 | if verbose: print("Found no {} cameras\n".format(backend)) 83 | return 84 | if verbose: print("Found {} {} camera{}".format(cam_num,backend,"s" if cam_num>1 else "")) 85 | for ci in cam_infos: 86 | if verbose: print("Found {} camera dev_idx={}, cam_idx={}".format(backend,ci.dev_id,ci.cam_id)) 87 | if verbose: print("\tModel {}, serial {}".format(ci.model,ci.serial_number)) 88 | yield None,(backend,ci) 89 | @classmethod 90 | def iterate_cameras(cls, verbose=False): 91 | for backend in ["uc480","ueye"]: 92 | for desc in cls._iterate_backend(backend,verbose=verbose): 93 | yield desc 94 | @classmethod 95 | def generate_description(cls, idx, cam=None, info=None): 96 | backend,ci=info 97 | cam_desc=cls.build_cam_desc(params={"idx":ci.cam_id,"dev_idx":ci.dev_id,"sn":ci.serial_number,"backend":backend}) 98 | cam_desc["display_name"]="{} {}".format(ci.model,ci.serial_number) 99 | cam_name="{}_{}".format(backend,idx) 100 | return cam_name,cam_desc 101 | 102 | def get_kind_name(self): 103 | return self._backend_names[self.settings.get("params/backend","uc480")] 104 | 105 | def make_thread(self, name): 106 | return UC480CameraThread(name=name,kwargs=self.settings["params"].as_dict()) 107 | 108 | def make_gui_control(self, parent): 109 | return Settings_GUI(parent,cam_desc=self) 110 | def make_gui_status(self, parent): 111 | return Status_GUI(parent,cam_desc=self) -------------------------------------------------------------------------------- /utils/gui/ActivityIndicator_ctl.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui import QtCore 2 | from pylablib.core.gui.widgets import container, param_table 3 | from pylablib.core.thread import controller 4 | from pylablib.core.utils import dictionary 5 | 6 | class ActivityIndicator_GUI(container.QWidgetContainer): 7 | """ 8 | Activity indicator widget. 9 | 10 | Displays statuses of different processes based on the resource manager. 11 | """ 12 | _ind_groups=["camera","saving","processing","misc"] 13 | _status_styles={"off":"", 14 | "on":"background:lightgreen; color: black", 15 | "pause":"background:green; color: black", 16 | "warning":"background:gold; color: black", 17 | "error":"background:red; color: black"} 18 | def setup(self, resource_manager_thread): 19 | super().setup(no_margins=True) 20 | self.setMinimumWidth(25) 21 | self.setMaximumWidth(25) 22 | self.resource_manager_thread=resource_manager_thread 23 | self.resource_manager=controller.sync_controller(self.resource_manager_thread) 24 | self.pinds={g:{} for g in self._ind_groups} 25 | # Setup GUI 26 | self.params=self.add_child("params",param_table.ParamTable(self)) 27 | self.params.setup(add_indicator=False) 28 | self.params.get_sublayout().setContentsMargins(0,0,0,0) 29 | for g in self._ind_groups: 30 | self.params.add_sublayout(g,kind="vbox") 31 | self.params.add_spacer(10) 32 | self.params.add_padding(stretch=1) 33 | # Timer 34 | self.add_timer_event("update_pinds",self.update_pinds,period=0.5) 35 | self.ctl.add_thread_method("update_activity_status",self.update_activity_status) 36 | def start(self): 37 | self.update_pinds() 38 | super().start() 39 | 40 | def update_activity_status(self, group, name, status): 41 | """Update an activity indicator status""" 42 | self.resource_manager.csi.update_resource("process_activity",group+"/"+name,status=status) 43 | self.set_pind(group,name,status) 44 | 45 | def _find_position(self, group, order=None): 46 | if order is None: 47 | order=max(self.pinds[group].values()) 48 | if not isinstance(order,tuple): 49 | order=(order,) 50 | ordlist=sorted((o,n) for n,o in self.pinds[group].items()) 51 | nxt=None 52 | for o,n in ordlist: 53 | if o>order: 54 | nxt=n 55 | break 56 | if nxt is None: 57 | return self.params.get_layout_shape(group)[0],order 58 | return self.params.get_element_position(self.params.w[group,nxt])[1][0],order 59 | def add_pind(self, group, name, caption=None, short_cap=None, order=None): 60 | """Add indicator to the table""" 61 | if caption is None: 62 | caption=name.capitalize() 63 | if short_cap is None: 64 | short_cap=caption[:3] 65 | g=self.pinds[group] 66 | if name in g: 67 | raise ValueError("process indicator {} already exists in group {}".format(name,group)) 68 | pos,order=self._find_position(group,order) 69 | g[name]=order 70 | self.params.add_text_label((group+"/"+name),location=(group,pos),value=short_cap,tooltip=caption) 71 | widget=self.params.w[group,name] 72 | widget.setMinimumWidth(20) 73 | widget.setMinimumHeight(20) 74 | widget.setAlignment(QtCore.Qt.AlignCenter) 75 | def remove_pind(self, group, name): 76 | """Remove indicator from the table""" 77 | if name not in self.pinds[group]: 78 | raise KeyError("process indicator {} does not exist in group {}".format(name,group)) 79 | del self.pinds[group][name] 80 | self.params.remove_widget((group,name)) 81 | def set_pind(self, group, name, status): 82 | """Set indicator status""" 83 | if (group,name) in self.params.w: 84 | self.params.w[group,name].setStyleSheet(self._status_styles[status]) 85 | 86 | def update_pinds(self): 87 | """Update GUI based on the specified resources""" 88 | pinds=self.resource_manager.cs.list_resources("process_activity") 89 | present=set() 90 | for i,d in pinds.items(): 91 | path=dictionary.split_path(i) 92 | group,name=path[0],"/".join(path[1:]) 93 | status=d.get("status","off") 94 | if name not in self.pinds[group]: 95 | self.add_pind(group,name,caption=d.get("caption"),short_cap=d.get("short_cap"),order=d.get("order")) 96 | self.set_pind(group,name,status) 97 | present.add((group,name)) 98 | for g in self.pinds: 99 | for n in list(self.pinds[g]): 100 | if (g,n) not in present: 101 | self.remove_pind(g,n) -------------------------------------------------------------------------------- /utils/gui/DisplaySettings_ctl.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui.widgets import container, param_table 2 | from pylablib.core.thread import controller 3 | 4 | import time 5 | 6 | 7 | class DisplaySettings_GUI(container.QGroupBoxContainer): 8 | """ 9 | Display settings controller widget. 10 | 11 | Shows and controls display FPS, and notifies if a slowdown is active. 12 | 13 | Args: 14 | slowdown_thread: name of the frame slowdown thread, if applicable 15 | period_update_tag: tag of a multicast which is sent when the display update period is changed 16 | """ 17 | _ignore_set_values={"slowdown_enabled"} 18 | def setup(self, slowdown_thread=None, period_update_tag="processing/control"): 19 | super().setup(caption="Display settings",no_margins=True) 20 | self.setFixedWidth(200) 21 | self.slowdown_thread=slowdown_thread 22 | self.frame_slowdown=controller.sync_controller(self.slowdown_thread) if self.slowdown_thread else None 23 | # Setup GUI 24 | self.params=self.add_child("params",param_table.ParamTable(self)) 25 | self.params.setup(add_indicator=False) 26 | self.params.add_decoration_label("Display update period:",location=("next",0,1,2)) 27 | self.params.add_num_edit("display_update_period",formatter=".3f",limiter=(0.01,None,"coerce"),value=0.05,location=(-1,2)) 28 | self.params.add_num_label("display_fps",formatter=".3f",label="Display FPS:",value=0) 29 | self.params.add_text_label("slowdown_enabled",location=(-1,2,1,"end")) 30 | def set_display_period(period): 31 | if period_update_tag is not None: 32 | self.ctl.send_multicast(tag=period_update_tag,value=("display_update_period",period)) 33 | self.params.vs["display_update_period"].connect(set_display_period) 34 | self._fps_update_period=1. 35 | self._last_fps_refresh=time.time() 36 | self._last_fps_cnt=0 37 | # Timer 38 | self.add_timer_event("update_params",self.update_params,period=0.5) 39 | def start(self): 40 | self.gui_values.update_value("display_update_period") 41 | super().start() 42 | 43 | def _refresh_fps(self): 44 | t=time.time() 45 | if self._last_fps_refresh+self._fps_update_period>t: 46 | self._last_fps_cnt+=1 47 | else: 48 | self.v["display_fps"]=(self._last_fps_cnt+1.)/(t-self._last_fps_refresh) 49 | self._last_fps_refresh=t 50 | self._last_fps_cnt=0 51 | def update_params(self): 52 | """Update parameters (display FPS indicator)""" 53 | t=time.time() 54 | if t>self._last_fps_refresh+2*self._fps_update_period: 55 | self.v["display_fps"]=self._last_fps_cnt/(t-self._last_fps_refresh) 56 | self._last_fps_refresh=t 57 | self._last_fps_cnt=0 58 | @controller.exsafeSlot() 59 | def on_new_frame(self): 60 | """Process generation of a new frame""" 61 | t=time.time() 62 | self._last_fps_cnt+=1 63 | if t>self._last_fps_refresh+self._fps_update_period: 64 | self.v["display_fps"]=self._last_fps_cnt/(t-self._last_fps_refresh) 65 | self._last_fps_refresh=t 66 | self._last_fps_cnt=0 67 | if self.frame_slowdown is not None and self.frame_slowdown.v["enabled"]: 68 | self.v["slowdown_enabled"]="slowdown" 69 | self.params.w["slowdown_enabled"].setStyleSheet("background: gold; font-weight: bold; color: black") 70 | else: 71 | self.v["slowdown_enabled"]="" 72 | self.params.w["slowdown_enabled"].setStyleSheet("") -------------------------------------------------------------------------------- /utils/gui/FramePreprocess_ctl.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui.widgets import container, param_table 2 | from pylablib.core.thread import controller 3 | 4 | 5 | class FramePreproccessBinning_GUI(container.QGroupBoxContainer): 6 | """ 7 | Frame preprocessing settings controller widget. 8 | 9 | Controls time and space binning parameters. 10 | """ 11 | def setup(self, preprocess_thread): 12 | super().setup(caption="Acquisition binning",no_margins=True) 13 | # Setup threads 14 | self.image_preprocessor=controller.sync_controller(preprocess_thread) 15 | # Setup GUI 16 | self.params=self.add_child("params",param_table.ParamTable(self)) 17 | self.params.setup(add_indicator=False) 18 | bin_modes=["mean","sum","min","max","skip"] 19 | bin_mode_names=["Mean","Sum","Min","Max","Skip"] 20 | self.params.add_combo_box("spat_bin_mode",options=bin_mode_names,index_values=bin_modes,value="mean",label="Spatial binning mode:") 21 | with self.params.using_new_sublayout("spat_bin_box","hbox"): 22 | self.params.add_num_edit("spat_bin_x",label="X:",formatter="int",limiter=(1,None,"coerce","int"),value=1) 23 | self.params.add_num_edit("spat_bin_y",label="Y:",formatter="int",limiter=(1,None,"coerce","int"),value=1) 24 | self.params.add_padding() 25 | self.params.add_spacer(0) 26 | self.params.add_combo_box("time_bin_mode",options=bin_mode_names,index_values=bin_modes,value="mean",label="Temporal binning mode:") 27 | with self.params.using_new_sublayout("time_bin_box","hbox"): 28 | self.params.add_num_edit("time_bin",label="T:",formatter="int",limiter=(1,None,"coerce","int"),value=1) 29 | self.params.add_padding() 30 | self.params.add_spacer(0) 31 | self.params.add_check_box("convert_to_float",caption="Convert frame to float") 32 | self.params.add_toggle_button("bin_enabled",caption="Enable binning") 33 | @controller.exsafe 34 | def setup_binning(): 35 | spat_bin=self.v["spat_bin_x"],self.v["spat_bin_y"] 36 | spat_bin_mode=self.v["spat_bin_mode"] 37 | time_bin=self.v["time_bin"] 38 | time_bin_mode=self.v["time_bin_mode"] 39 | convert_to_float=self.v["convert_to_float"] 40 | self.image_preprocessor.ca.setup_binning(spat_bin,spat_bin_mode,time_bin,time_bin_mode,dtype="float" if convert_to_float else None) 41 | for ctl in ["spat_bin_mode","spat_bin_x","spat_bin_y","time_bin_mode","time_bin","convert_to_float"]: 42 | self.params.vs[ctl].connect(setup_binning) 43 | setup_binning() 44 | self.params.vs["bin_enabled"].connect(self.enable_binning) 45 | self.params.add_padding("horizontal",location=(0,"next","end",1)) 46 | self.params.layout().setColumnStretch(1,0) 47 | for ctl in ["spat_bin_x","spat_bin_y","time_bin"]: 48 | self.params.w[ctl].setMaximumWidth(70) 49 | self.ctl.res_mgr.cs.add_resource("process_activity","processing/binning", 50 | caption="Binning",short_cap="Bin",order=0) 51 | def enable_binning(self, enable): 52 | self.image_preprocessor.ca.enable_binning(enable) 53 | self.ctl.call_thread_method("update_activity_status","processing","binning",status="on" if enable else "off") 54 | 55 | 56 | class FramePreproccessSlowdown_GUI(container.QGroupBoxContainer): 57 | """ 58 | Frame slowdown settings controller widget. 59 | 60 | Controls target FPS and slowdown buffer size. 61 | """ 62 | def setup(self, slowdown_thread): 63 | super().setup(caption="Slowdown",no_margins=True) 64 | # Setup threads 65 | self.frame_slowdown=controller.sync_controller(slowdown_thread) 66 | # Setup GUI 67 | self.params=self.add_child("params",param_table.ParamTable(self)) 68 | self.params.setup() 69 | 70 | self.params.add_num_label("source_fps",formatter=".1f",label="Source FPS:") 71 | self.params.add_num_edit("slowdown_fps",formatter=".1f",limiter=(1,None,"coerce"),value=10,label="Target FPS:") 72 | self.params.add_num_edit("slowdown_buffer",formatter="int",limiter=(1,None,"coerce","int"),value=100,label="Slowdown buffer:") 73 | self.params.add_toggle_button("slowdown_enabled",caption="Slowdown",add_indicator=False) 74 | @controller.exsafe 75 | def setup_slowdown(): 76 | self.frame_slowdown.ca.setup_slowdown(self.v["slowdown_fps"],self.v["slowdown_buffer"]) 77 | for ctl in ["slowdown_fps","slowdown_buffer"]: 78 | self.params.vs[ctl].connect(setup_slowdown) 79 | setup_slowdown() 80 | self.params.vs["slowdown_enabled"].connect(lambda v: self.frame_slowdown.ca.enable(v)) 81 | self.params.add_padding("horizontal",location=(0,"next","end",1)) 82 | self.params.layout().setColumnStretch(1,0) 83 | for ctl in ["slowdown_fps","slowdown_buffer"]: 84 | self.params.w[ctl].setMaximumWidth(70) 85 | # Timer 86 | self.add_timer_event("recv_parameters",self.recv_parameters,period=0.5) 87 | def recv_parameters(self): 88 | """Update slowdown indicators""" 89 | self.v["source_fps"]=self.frame_slowdown.v["fps/in"] 90 | self.i["slowdown_fps"]=self.frame_slowdown.v["fps/out"] 91 | self.i["slowdown_buffer"]="{} / {}".format(self.frame_slowdown.v["buffer/used"],self.frame_slowdown.v["buffer/filled"]) 92 | if self.frame_slowdown.v["buffer/empty"]: 93 | self.v["slowdown_enabled"]=False -------------------------------------------------------------------------------- /utils/gui/FrameProcess_ctl.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui.widgets import container, param_table 2 | from pylablib.core.thread import controller 3 | from pylablib.gui.widgets import range_controls 4 | 5 | 6 | class FrameProccess_GUI(container.QGroupBoxContainer): 7 | """ 8 | Frame processing settings controller widget. 9 | 10 | Controls enabling processing steps and triggering background acquisition. 11 | """ 12 | def setup(self, process_thread, settings=None): 13 | super().setup(caption="Background subtraction",no_margins=True) 14 | # Setup threads 15 | self.process_thread=process_thread 16 | self.settings=settings or {} 17 | self.image_processor=controller.sync_controller(self.process_thread) 18 | self.image_processor.ca.load_settings(self.settings) 19 | # Setup GUI 20 | self.params=self.add_child("params",param_table.ParamTable(self)) 21 | self.params.setMaximumWidth(260) 22 | self.params.setup(add_indicator=False) 23 | self.params.add_combo_box("method",options=["Snapshot","Running"],index_values=["snapshot","running"],label="Method:") 24 | self.params.add_num_edit("comb_count",formatter="int",limiter=(1,None,"coerce","int"),value=1,label="Frames count:",add_indicator=True) 25 | self.params.add_num_edit("comb_step",formatter="int",limiter=(1,None,"coerce","int"),value=1,label="Frames step:",add_indicator=True) 26 | self.params.add_combo_box("comb_mode",options=["Mean","Median","Min"],index_values=["mean","median","min"],label="Combination mode:") 27 | self.params.add_spacer(0) 28 | self.params.add_button("grab_background","Grab background",location=("next",0,1,1)) 29 | self.params.add_text_label("background_state",location=(-1,1,1,"end")) 30 | with self.params.using_new_sublayout("background_saving_row","hbox"): 31 | self.params.add_combo_box("background_saving",options=["None","Only background","Background + source"],index_values=["none","background","all"],label="Snap save:") 32 | self.params.add_padding(stretch=1) 33 | self.params.add_spacer(5) 34 | self.params.add_toggle_button("enabled",caption="Enable subtraction") 35 | self.params.add_spacer(width=60,location=(0,-1)) 36 | # Signals 37 | @controller.exsafe 38 | def setup_subtraction(): 39 | enabled=self.v["enabled"] 40 | method=self.v["method"] 41 | if method=="snapshot": 42 | self.image_processor.csi.setup_snapshot_saving(self.v["background_saving"]) 43 | self.image_processor.csi.setup_snapshot_subtraction(self.v["comb_count"],self.v["comb_mode"],self.v["comb_step"]) 44 | else: 45 | self.image_processor.csi.setup_running_subtraction(self.v["comb_count"],self.v["comb_mode"],self.v["comb_step"]) 46 | self.image_processor.csi.setup_subtraction_method(method=method,enabled=enabled) 47 | self.recv_parameters() 48 | for w in ["method","enabled","comb_count","comb_step","comb_mode","background_saving"]: 49 | self.params.vs[w].connect(setup_subtraction) 50 | self.params.vs["grab_background"].connect(lambda: self.image_processor.ca.grab_snapshot_background()) 51 | # Timer 52 | self.add_timer_event("recv_parameters",self.recv_parameters,period=0.5) 53 | self.setEnabled(False) 54 | self.ctl.res_mgr.cs.add_resource("process_activity","processing/background", 55 | caption="Background subtraction",short_cap="Bg",order=1) 56 | 57 | 58 | @controller.exsafe 59 | def recv_parameters(self): 60 | """Update frame processing indicators (snapshot background mode and count)""" 61 | if self.image_processor.v["overridden"]: 62 | self.setEnabled(False) 63 | else: 64 | self.setEnabled(True) 65 | method=self.v["method"] 66 | is_snapshot=method=="snapshot" 67 | self.params.set_enabled(["grab_background","background_state"],is_snapshot) 68 | self.params.set_enabled(["background_saving"],is_snapshot and self.v["enabled"]) 69 | self.i["comb_count"]="{} / {}".format(self.image_processor.v[method,"grabbed"],self.image_processor.v[method,"parameters/count"]) 70 | bg_state=self.image_processor.v["snapshot/background/state"] 71 | self.v["background_state"]={"none":"Not acquired","acquiring":"Accumulating","valid":"Valid","wrong_size":"Wrong size"}[bg_state] 72 | enabled=False 73 | if self.image_processor.v["enabled"]: 74 | method=self.image_processor.v["method"] 75 | if method=="snapshot": 76 | enabled=self.image_processor.v["snapshot/background/state"]=="valid" 77 | elif method=="running": 78 | enabled=self.image_processor.v["running/background/frame"] is not None 79 | self.ctl.call_thread_method("update_activity_status","processing","background",status="on" if enabled else "off") -------------------------------------------------------------------------------- /utils/gui/PlotControl_ctl.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui.widgets import container, param_table 2 | from pylablib.core.thread import controller 3 | 4 | 5 | 6 | class PlotControl_GUI(container.QGroupBoxContainer): 7 | """ 8 | Filter settings controller widget. 9 | 10 | Controls loading and enabling filters, manages their controls. 11 | """ 12 | def setup(self, channel_accumulator_thread, plot_window, settings=None): 13 | super().setup(caption="Time series plot",no_margins=True) 14 | # Setup threads 15 | self.channel_accumulator_thread=channel_accumulator_thread 16 | self.settings=settings or {} 17 | self.channel_accumulator=controller.sync_controller(self.channel_accumulator_thread) 18 | 19 | # Setup plot window 20 | self.plot_window=plot_window 21 | self.plot_window.setLabel("left","Signal") 22 | self.plot_window.showGrid(True,True,0.7) 23 | self.plot_lines={} 24 | self.plot_markers={} 25 | # Setup control table 26 | self.params=self.add_child("params",param_table.ParamTable(self)) 27 | self.params.setup(add_indicator=False) 28 | self.params.add_toggle_button("enable","Enable").get_value_changed_signal().connect(self.enable) 29 | self.params.add_combo_box("source",options=["Display frame mean","Raw frame mean"],index_values=["show","raw"],label="Source") 30 | self.params.vs["source"].connect(self.change_source) 31 | self.params.add_num_edit("skip_count",1,limiter=(1,None,"coerce","int"),formatter=("int"),label="Calculate every: ") 32 | self.params.add_check_box("roi/enable","Use ROI").get_value_changed_signal().connect(self.setup_roi) 33 | with self.params.using_new_sublayout("roi","grid"): 34 | self.params.add_num_edit("roi/center/x",value=0,limiter=(0,None,"coerce","int"),formatter="int",label="Center: ") 35 | self.params.add_num_edit("roi/center/y",value=0,limiter=(0,None,"coerce","int"),formatter="int",location=(-1,2)) 36 | self.params.add_num_edit("roi/size/x",value=0,limiter=(1,None,"coerce","int"),formatter="int",label="Size: ") 37 | self.params.add_num_edit("roi/size/y",value=0,limiter=(1,None,"coerce","int"),formatter="int",location=(-1,2)) 38 | self.params.add_button("roi/reset","Reset ROI").get_value_changed_signal().connect(self.reset_roi) 39 | for n in ["center/x","center/y","size/x","size/y"]: 40 | self.params.w["roi/"+n].setMaximumWidth(60) 41 | self.params.vs["roi/"+n].connect(self.setup_roi) 42 | self.params.add_spacer(10) 43 | self.params.add_toggle_button("update_plot","Update plot") 44 | self.params.add_num_edit("disp_last",1000,limiter=(1,None,"coerce","int"),formatter=("int"),label="Display last: ") 45 | self.params.add_button("reset_history","Reset history").get_value_changed_signal().connect(lambda: self.channel_accumulator.ca.reset()) 46 | self.params.add_padding("horizontal",location=(0,"next")) 47 | self.params.layout().setColumnStretch(1,0) 48 | self.params.contained_value_changed.connect(self.setup_gui_state) 49 | self._update_roi_display((0,0),(0,0)) 50 | self.setup_gui_state() 51 | self.params.vs["skip_count"].connect(self.setup_processing) 52 | self.enable(False) 53 | self.change_source("show") 54 | self.add_timer_event("update_plot",self.update_plot,period=0.1) 55 | 56 | @controller.exsafe 57 | def enable(self, enabled): 58 | """Enable time series accumulation and plotting""" 59 | self.plot_window.setVisible(enabled) 60 | self.channel_accumulator.ca.enable(enabled) 61 | self.channel_accumulator.ca.reset() 62 | @controller.exsafe 63 | def change_source(self, src): 64 | """Set the frames source and changed the plot display accordingly""" 65 | self.channel_accumulator.ca.select_source(src) 66 | if src=="show": 67 | self.plot_window.setLabel("bottom","Time") 68 | else: 69 | self.plot_window.setLabel("bottom","Frame index") 70 | self._setup_plot_channels(["mean"],["Mean intensity"]) 71 | @controller.exsafeSlot() 72 | def setup_processing(self): 73 | self.channel_accumulator.ca.setup_processing(skip_count=self.v["skip_count"]) 74 | @controller.exsafeSlot() 75 | def setup_gui_state(self): 76 | """Enable or disable controls based on which actions are enabled""" 77 | enabled=self.v["enable"] 78 | roi_enabled=self.v["roi/enable"] 79 | update_plot=self.v["update_plot"] 80 | raw_frame_source=self.v["source"]=="raw" 81 | self.params.set_enabled(["source","skip_count","roi/enable","update_plot","reset_history"],enabled) 82 | self.params.set_enabled("skip_count",enabled and raw_frame_source) 83 | self.params.set_enabled("roi/enable",enabled) 84 | self.params.set_enabled("disp_last",enabled and update_plot) 85 | for name in ["center/x","center/y","size/x","size/y","reset"]: 86 | self.params.set_enabled("roi/"+name,enabled and roi_enabled) 87 | if enabled and roi_enabled: 88 | self.ctl.send_multicast(tag="image_plotter/control",value=("rectangles/show","mean_plot_roi")) 89 | else: 90 | self.ctl.send_multicast(tag="image_plotter/control",value=("rectangles/hide","mean_plot_roi")) 91 | 92 | def _update_roi_display(self, center, size): 93 | self.ctl.send_multicast(tag="image_plotter/control",value=("rectangles/set",("mean_plot_roi",center,size))) 94 | @controller.exsafeSlot() 95 | def reset_roi(self): 96 | """Reset ROI to the whole image""" 97 | new_roi=self.channel_accumulator.cs.reset_roi() 98 | if new_roi is not None: 99 | self.v["roi/center/x"]=new_roi.center()[0] 100 | self.v["roi/center/y"]=new_roi.center()[1] 101 | self.v["roi/size/x"]=new_roi.size()[0] 102 | self.v["roi/size/y"]=new_roi.size()[1] 103 | self._update_roi_display(new_roi.center(),new_roi.size()) 104 | @controller.exsafeSlot() 105 | def setup_roi(self): 106 | """Update ROI parameters""" 107 | center=self.v["roi/center/x"],self.v["roi/center/y"] 108 | size=self.v["roi/size/x"],self.v["roi/size/y"] 109 | enabled=self.v["roi/enable"] 110 | self.channel_accumulator.ca.setup_roi(center=center,size=size,enabled=enabled) 111 | self._update_roi_display(center,size) 112 | 113 | def _setup_plot_channels(self, channels=None, labels=None, enabled=None): 114 | """Setup plot channel names and labels""" 115 | channels=channels or [] 116 | labels=labels or [None]*len(channels) 117 | enabled=enabled or [True]*len(channels) 118 | if self.plot_window.plotItem.legend: 119 | self.plot_window.plotItem.legend.scene().removeItem(self.plot_window.plotItem.legend) 120 | self.plot_window.plotItem.legend=None 121 | for ch in self.plot_lines: 122 | self.plot_window.removeItem(self.plot_lines[ch]) 123 | self.plot_window.removeItem(self.plot_markers[ch]) 124 | if labels and any([l is not None for l in labels]): 125 | self.plot_window.addLegend() 126 | self.plot_lines={} 127 | self.plot_markers={} 128 | mpl_colors=['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#bcbd22','#17becf'] 129 | for i,(ch,l,en) in enumerate(zip(channels,labels,enabled)): 130 | if ch not in self.plot_lines: 131 | if ch=="idx": 132 | self.plot_window.setLabel("bottom",l or "") 133 | elif en: 134 | col=mpl_colors[i%len(mpl_colors)] 135 | self.plot_lines[ch]=self.plot_window.plot([],[],pen=col,name=l) 136 | self.plot_markers[ch]=self.plot_window.plot([],[],symbolBrush=col,symbol="o",symbolSize=5,pxMode=True) 137 | 138 | @controller.exsafe 139 | def update_plot(self): 140 | """Update frame processing indicators""" 141 | if self.v["update_plot"]: 142 | channels=self.channel_accumulator.csi.get_data(maxlen=self.v["disp_last"]) 143 | if channels: 144 | idx=channels["idx"] 145 | for ch in self.plot_lines: 146 | if ch in channels: 147 | data=channels[ch] 148 | self.plot_lines[ch].setData(idx,data) 149 | if len(idx)>0: 150 | self.plot_markers[ch].setData([idx[-1]],[data[-1]]) -------------------------------------------------------------------------------- /utils/gui/ProcessingIndicator_ctl.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui.widgets import param_table 2 | from pylablib.core.thread import controller 3 | 4 | 5 | class ProcessingIndicator_GUI(param_table.ParamTable): 6 | """ 7 | Processing steps display above the plot windows. 8 | 9 | Shows and controls display FPS, and notifies if a slowdown is active. 10 | 11 | Args: 12 | items: a list of step items ``[(name,(caption,getter))]``, where ``name`` is the item name, 13 | ``caption`` is its caption (shown in the steps table), 14 | and ``getter`` is a function which returns a string with the step representation or ``None`` if the step is disabled. 15 | update: if ``True``, update indicators right after setup. 16 | """ 17 | def setup(self, items, update=True): 18 | super().setup(add_indicator=False) 19 | self.items=items 20 | for name,(caption,_) in items: 21 | with self.using_new_sublayout(name,"hbox"): 22 | self.add_text_label(name,label="{}: ".format(caption)) 23 | self.add_padding() 24 | if update: 25 | self.update_indicators() 26 | self._ignore_set_values={name for name,_ in items} 27 | def _set_value(self, name, value, default="none"): 28 | self.w[name].setStyleSheet(None if value is None else "font-weight: bold") 29 | self.v[name]=default if value is None else value 30 | @controller.exsafe 31 | def update_indicators(self): 32 | """Update processing step indicators""" 33 | for name,(_,getter) in self.items: 34 | self._set_value(name,getter()) 35 | 36 | 37 | 38 | def binning_item(preprocessor): 39 | """Create an processing step item based on a binning thread""" 40 | ctl=controller.sync_controller(preprocessor) 41 | def getter(): 42 | params=ctl.v["params"] 43 | if ctl.v["enabled"]: 44 | spat_bin="{}x{} spatial".format(*params["spat/bin"]) if params["spat/bin"]!=(1,1) else "" 45 | temp_bin="{} temporal".format(params["time/bin"]) if params["time/bin"]!=1 else "" 46 | if spat_bin or temp_bin: 47 | return ", ".join([i for i in [spat_bin,temp_bin] if i]) 48 | return None 49 | return "Binning",getter 50 | 51 | def background_item(processor): 52 | """Create an processing step item based on a background subtraction thread thread""" 53 | ctl=controller.sync_controller(processor) 54 | def getter(): 55 | if ctl.v["enabled"]: 56 | method=ctl.v["method"] 57 | if method=="snapshot" and ctl.v["snapshot/background/state"]!="valid": 58 | return None 59 | if method=="running" and ctl.v["running/background/frame"] is None: 60 | return None 61 | return method.capitalize() 62 | return None 63 | return "Background subtraction",getter -------------------------------------------------------------------------------- /utils/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexShkarin/pyLabLib-cam-control/3fc5a14670bd31f0549a5791f08e8a01c0013062/utils/gui/__init__.py -------------------------------------------------------------------------------- /utils/gui/about.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui import QtCore, QtWidgets 2 | from pylablib import widgets 3 | from .. import version 4 | 5 | 6 | class AboutBox(widgets.QFrameContainer): 7 | """Window with the 'About' info""" 8 | def _increase_font(self, widget, factor): 9 | font=widget.font() 10 | font.setPointSize(int(font.pointSize()*factor)) 11 | widget.setFont(font) 12 | def setup(self): 13 | super().setup() 14 | self.setWindowTitle("About") 15 | self.setWindowFlag(QtCore.Qt.Dialog) 16 | self.setWindowFlag(QtCore.Qt.WindowMaximizeButtonHint,False) 17 | self.setWindowFlag(QtCore.Qt.WindowMinimizeButtonHint,False) 18 | self.setWindowModality(QtCore.Qt.WindowModal) 19 | self._increase_font(self.add_decoration_label("PyLabLib cam-control"),1.6) 20 | self._increase_font(self.add_decoration_label("Version {}".format(version)),1.3) 21 | self.add_spacer(10) 22 | with self.using_new_sublayout("links","grid"): 23 | self.add_decoration_label("Documentation") 24 | link="https://pylablib-cam-control.readthedocs.io/" 25 | self.add_decoration_label("{link:}".format(link=link),location=(-1,1)).setOpenExternalLinks(True) 26 | self.add_decoration_label("Check for a new version") 27 | link="https://pylablib-cam-control.readthedocs.io/en/latest/changelog.html" 28 | self.add_decoration_label("{link:}".format(link=link),location=(-1,1)).setOpenExternalLinks(True) 29 | self.add_decoration_label("Report a problem") 30 | link="https://github.com/SandoghdarLab/pyLabLib-cam-control/issues" 31 | self.add_decoration_label("{link:}".format(link=link),location=(-1,1)).setOpenExternalLinks(True) 32 | self.add_decoration_label("E-mail") 33 | link="pylablib@gmail.com" 34 | self.add_decoration_label("{link:}".format(link=link),location=(-1,1)).setOpenExternalLinks(True) 35 | self.layout().setSizeConstraint(QtWidgets.QLayout.SetFixedSize) -------------------------------------------------------------------------------- /utils/gui/color_theme.py: -------------------------------------------------------------------------------- 1 | 2 | from pylablib.core.gui import is_pyside2 3 | from pylablib.core.utils import funcargparse 4 | import qdarkstyle 5 | 6 | import re 7 | 8 | def load_style(style="light"): 9 | """ 10 | Load color theme style. 11 | 12 | Can be ``"standard"`` (default OS style), ``"dark"`` (qdarkstyle dark), or ``"light"`` (qdarkstyle light). 13 | """ 14 | funcargparse.check_parameter_range(style,"style",["standard","light","dark"]) 15 | if style=="standard": 16 | return "" 17 | palette=qdarkstyle.DarkPalette if style=="dark" else qdarkstyle.LightPalette 18 | accent_color="#406482" if style=="dark" else "#94c1e0" 19 | accent_hover_color="#254f73" if style=="dark" else "#5a96bf" 20 | checked_style="\n\nQPushButton:checked {{background-color: {};}}\n\nQPushButton:checked:hover {{background-color: {};}}".format(accent_color,accent_hover_color) 21 | stylesheet=qdarkstyle.load_stylesheet(pyside=is_pyside2,palette=palette) 22 | m=re.search(r"QPushButton:checked\s*{[^}]*}",stylesheet,flags=re.DOTALL) 23 | end=m.span()[1] 24 | stylesheet=stylesheet[:end]+checked_style+stylesheet[end:] 25 | return stylesheet 26 | -------------------------------------------------------------------------------- /utils/gui/error_message.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui import QtCore, QtWidgets 2 | 3 | class ErrorBox(QtWidgets.QFrame): 4 | """Window with the error info""" 5 | def setup(self, error_msg): 6 | self.setWindowTitle("Error") 7 | self.setWindowFlag(QtCore.Qt.Dialog) 8 | self.setWindowFlag(QtCore.Qt.WindowMaximizeButtonHint,False) 9 | self.setWindowFlag(QtCore.Qt.WindowMinimizeButtonHint,False) 10 | self.setWindowModality(QtCore.Qt.WindowModal) 11 | self.setMaximumWidth(500) 12 | self.main_layout=QtWidgets.QHBoxLayout(self) 13 | icon=self.style().standardIcon(self.style().SP_MessageBoxCritical) 14 | self.icon_label=QtWidgets.QLabel(parent=self) 15 | self.icon_label.setPixmap(icon.pixmap(64,64)) 16 | self.main_layout.addWidget(self.icon_label,0,QtCore.Qt.AlignTop) 17 | self.text_layout=QtWidgets.QVBoxLayout() 18 | self.main_layout.addLayout(self.text_layout) 19 | self.header_label=QtWidgets.QLabel(text="An error occurred",parent=self) 20 | self.text_layout.addWidget(self.header_label) 21 | self.message_label=QtWidgets.QLabel(text=" "+error_msg,parent=self) 22 | self.message_label.setWordWrap(True) 23 | self.text_layout.addWidget(self.message_label) 24 | link="https://github.com/SandoghdarLab/pyLabLib-cam-control/issues" 25 | email="pylablib@gmail.com" 26 | contact_text="If the error keeps occuring, contact the developer on GitHub or via email at {email:}".format(link=link,email=email) 27 | self.contact_label=QtWidgets.QLabel(text=contact_text,parent=self) 28 | self.contact_label.setOpenExternalLinks(True) 29 | self.text_layout.addWidget(self.contact_label) 30 | self.exit_button=QtWidgets.QPushButton(text="OK",parent=self) 31 | self.exit_button.clicked.connect(self.close) 32 | self.exit_button.setFixedWidth(self.exit_button.width()) 33 | self.text_layout.addWidget(self.exit_button,0,QtCore.Qt.AlignRight) 34 | self.layout().setSizeConstraint(QtWidgets.QLayout.SetFixedSize) -------------------------------------------------------------------------------- /utils/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .framestream import FrameProcessorThread, FrameBinningThread, FrameSlowdownThread, ChannelAccumulator, FrameSaveThread 2 | from .misc import SettingsManager, ResourceManager, GarbageCollector -------------------------------------------------------------------------------- /utils/services/dev.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.gui import utils as gui_utils, QtCore 2 | 3 | import os 4 | 5 | def crop(img, left=0, right=None, top=0, bottom=None): 6 | """Crop QPixmap to the given rectangle""" 7 | w,h=img.size().width(),img.size().height() 8 | left=left%w 9 | right=right%w if right is not None else w 10 | top=top%h 11 | bottom=bottom%h if bottom is not None else h 12 | rect=QtCore.QRect(left,top,right-left,bottom-top) 13 | return img.copy(rect) 14 | def take_screenshots(src): 15 | """Take screenshots for the documentation""" 16 | sfolder="screenshots/" 17 | os.makedirs(sfolder,exist_ok=True) 18 | if src.compact_interface: 19 | gui_utils.get_screenshot(src).save(sfolder+"overview_compact.png") 20 | else: 21 | gui_utils.get_screenshot(src).save(sfolder+"overview.png") 22 | src.c["plot_tabs"].set_by_name("standard_frame") 23 | src.ctl.sleep(0.1) 24 | gui_utils.get_screenshot(widget=src.c["plot_tabs"]).save(sfolder+"interface_image_display.png") 25 | gui_utils.get_screenshot(widget=src.c["params_loading_settings"]).save(sfolder+"interface_footer.png") 26 | src.c["control_tabs"].set_by_name("cam_tab") 27 | src.ctl.sleep(0.1) 28 | gui_utils.get_screenshot(widget=src.c["cam_controller/settings"].parentWidget(),border=(17,17,38,3)).save(sfolder+"interface_camera_settings.png") 29 | gui_utils.get_screenshot(widget=src.c["cam_controller/camstat"]).save(sfolder+"interface_camera_status.png") 30 | gui_utils.get_screenshot(widget=src.c["cam_controller/savebox"],border=(3,2)).save(sfolder+"interface_save_control.png") 31 | gui_utils.get_screenshot(widget=src.c["cam_controller/savestat"]).save(sfolder+"interface_save_status.png") 32 | src.c["control_tabs"].set_by_name("proc_tab") 33 | src.ctl.sleep(0.1) 34 | gui_utils.get_screenshot(widget=src.c["control_tabs/proc_tab"]).save(sfolder+"interface_processing.png") 35 | src.c["control_tabs"].set_by_name("proc_tab") 36 | src.ctl.sleep(0.1) 37 | crop(gui_utils.get_screenshot(widget=src.c["control_tabs/proc_tab"],border=(9,9,29,3)),bottom=540).save(sfolder+"interface_processing.png") 38 | src.v["plotting/enable"]=True 39 | src.ctl.sleep(0.1) 40 | crop(gui_utils.get_screenshot(widget=src.c["control_tabs/proc_tab"]),top=512,bottom=810).save(sfolder+"interface_time_plot.png") 41 | src.v["plotting/enable"]=False 42 | src.ctl.sleep(0.1) 43 | crop(gui_utils.get_screenshot(widget=src.c["activity_indicator"],border=3),bottom=195).save(sfolder+"interface_activity.png") 44 | src.c["control_tabs"].set_by_name("filter.filt/ctl_tab") 45 | src.ctl.sleep(0.1) 46 | crop(gui_utils.get_screenshot(widget=src.c["control_tabs/filter.filt/ctl_tab"],border=(9,9,29,3)),bottom=332).save(sfolder+"interface_filter.png") 47 | src.c["control_tabs"].set_by_name("plugins") 48 | src.ctl.sleep(0.1) 49 | gui_utils.get_screenshot(widget=src.c["control_tabs/plugins/trigger_save.trigsave/params"],border=(9,9,29,3)).save(sfolder+"interface_save_trigger.png") 50 | src.call_extra("settings_editor") 51 | src.settings_editor.tabs.setCurrentIndex(1) 52 | src.ctl.sleep(0.1) 53 | gui_utils.get_screenshot(window=src.settings_editor).save(sfolder+"interface_preferences.png") 54 | src.settings_editor.close() 55 | src.call_extra("tutorial") 56 | src.ctl.sleep(0.1) 57 | gui_utils.get_screenshot(window=src.tutorial_box).save(sfolder+"interface_tutorial.png") 58 | src.tutorial_box.close() 59 | if "show_attributes_window" in src.c["cam_controller/settings"].advanced_params: 60 | window=src.c["cam_controller/settings"].advanced_params.c["attributes_window"] 61 | window.show() 62 | window.tabs.set_by_name("value") 63 | src.ctl.sleep(0.1) 64 | gui_utils.get_screenshot(window=window).save(sfolder+"interface_camera_attributes.png") 65 | window.tabs.set_by_name("value_props") 66 | src.ctl.sleep(0.1) 67 | gui_utils.get_screenshot(window=window).save(sfolder+"interface_camera_attributes_settings.png") 68 | src.tutorial_box.close() 69 | 70 | def on_key_press(src, event): 71 | """Execute dev functions based on the pressed keys""" 72 | if event.modifiers()&QtCore.Qt.ControlModifier and event.modifiers()&QtCore.Qt.ShiftModifier and event.key()==QtCore.Qt.Key_S: 73 | take_screenshots(src) -------------------------------------------------------------------------------- /utils/services/misc.py: -------------------------------------------------------------------------------- 1 | from pylablib.core.thread import controller 2 | from pylablib.core.utils import dictionary 3 | 4 | import threading 5 | import gc 6 | 7 | 8 | class SettingsManager(controller.QTaskThread): 9 | """ 10 | Settings manager. 11 | 12 | Keeps track of all the settings sources (each settings source can add more of them), 13 | usually in order to save them when the data is being saved. 14 | """ 15 | def setup_task(self): 16 | self.sources={} 17 | self.settings={} 18 | self.add_command("add_source") 19 | self.add_command("update_settings") 20 | self.add_command("get_all_settings") 21 | 22 | def add_source(self, name, func): 23 | """Add settings source as a function (called when settings values are requested)""" 24 | self.sources[name]=func 25 | def update_settings(self, name, settings): 26 | """Add settings values directly""" 27 | self.settings[name]=settings 28 | 29 | def get_all_settings(self, include=None, exclude=None, alias=None): 30 | """ 31 | Get all settings values 32 | 33 | If `include` is not ``None``, it specifies a list of setting sources to include (by default, all sources). 34 | If `exclude` is not ``None``, it specifies a list of setting sources to exclude (by default, none are excluded). 35 | If `alias` is not ``None``, specifies aliases (i.e., different names in the resulting dictionary) for settings nodes. 36 | """ 37 | settings=dictionary.Dictionary() 38 | alias=alias or {} 39 | for s in self.sources: 40 | if ((include is None) or (s in include)) and ((exclude is None) or (s not in exclude)): 41 | sett=self.sources[s]() 42 | settings.update({alias.get(s,s):sett}) 43 | for s in self.settings: 44 | if ((include is None) or (s in include)) and ((exclude is None) or (s not in exclude)) and (s not in settings): 45 | sett=self.settings[s] 46 | settings.update({alias.get(s,s):sett}) 47 | return settings 48 | 49 | 50 | 51 | class GarbageCollector(controller.QTaskThread): 52 | def setup_task(self, disabled=False): 53 | self.disabled=disabled 54 | self.v["enabled"]=not self.disabled 55 | self.add_job("garbage_collect",self.garbage_collect,2) 56 | def garbage_collect(self): 57 | if self.v["enabled"]: 58 | gc.collect() 59 | def setup(self, period=None, enabled=None): 60 | if period is not None: 61 | self.change_job_period("garbage_collect",period) 62 | if enabled is not None: 63 | self.v["enabled"]=enabled and not self.disabled 64 | 65 | 66 | 67 | 68 | class ResourceManager(controller.QTaskThread): 69 | """ 70 | Thread which manages information about broadly defined resources. 71 | 72 | Can add, get, update, or remove information about resources of different kind. 73 | 74 | Commands: 75 | - ``add_resource``: add a resource information 76 | - ``get_resource``: get values of a resource 77 | - ``list_resources``: get all resources of a given kind 78 | - ``update_resource``: update value of an already created resource 79 | - ``remove_resource``: remove the resource information 80 | """ 81 | def setup_task(self): 82 | super().setup_task() 83 | self._lock=threading.Lock() 84 | self.resources={} 85 | self._updaters={} 86 | self.add_direct_call_command("add_resource") 87 | self.add_direct_call_command("get_resource") 88 | self.add_direct_call_command("list_resources") 89 | self.add_direct_call_command("update_resource") 90 | self.add_direct_call_command("add_multicast_updater") 91 | self.add_direct_call_command("remove_resource") 92 | 93 | def add_resource(self, kind, name, ctl=None, **kwargs): 94 | """ 95 | Add a resource with the given kind and name. 96 | 97 | If `ctl` is not ``None``, can specify a resource-owning thread controller; 98 | if the controller is closed, the resource is automatically removed. 99 | `kwargs` specify the initial values of the resource. 100 | """ 101 | with self._lock: 102 | if kind not in self.resources: 103 | self.resources[kind]={} 104 | if name in self.resources[kind]: 105 | raise ValueError("resource {}/{} already exists".format(kind,name)) 106 | self.resources[kind][name]=kwargs 107 | if ctl is not None: 108 | ctl.add_stop_notifier(lambda: self.remove_resource(kind,name)) 109 | value=kwargs.copy() 110 | self.send_multicast(tag="resource/added",value=(kind,name,value)) 111 | self.send_multicast(tag="resource/{}/added".format(kind),value=(name,value)) 112 | def get_resource(self, kind, name, default=None): 113 | """ 114 | Get value of the resource with the given kind and name. 115 | 116 | If kind or name are not present, return `default`. 117 | """ 118 | with self._lock: 119 | if kind in self.resources and name in self.resources[kind]: 120 | return self.resources[kind][name] 121 | return default 122 | def list_resources(self, kind): 123 | """ 124 | List all resources of the given kind. 125 | 126 | Return a dictionary ``{kind: value}`` for all the resources. 127 | """ 128 | with self._lock: 129 | return {k:v.copy() for k,v in self.resources.get(kind,{}).items()} 130 | def update_resource(self, kind, name, **kwargs): 131 | """ 132 | Update value of the resource with the given kind and name. 133 | 134 | `kwargs` specify the values which need to be changed. 135 | """ 136 | with self._lock: 137 | if kind in self.resources and name in self.resources[kind]: 138 | self.resources[kind][name].update(kwargs) 139 | value=self.resources[kind][name].copy() 140 | self.send_multicast(tag="resource/updated",value=(kind,name,value)) 141 | self.send_multicast(tag="resource/{}/updated".format(kind),value=(name,value)) 142 | def add_multicast_updater(self, kind, name, updater, srcs="any", tags=None, dsts="any"): 143 | """ 144 | Add auto-updater which updates a resource based on an incoming multicast. 145 | 146 | `updater` is a function which takes 3 arguments (``src``, ``tag``, and ``value``) 147 | and returns an update dictionary (or ``None`` if no update is necessary). 148 | """ 149 | def do_update(src, tag, value): 150 | params=updater(src,tag,value) or {} 151 | self.update_resource(kind,name,**params) 152 | sid=self.subscribe_direct(do_update,srcs=srcs,tags=tags,dsts=dsts) 153 | self._updaters.setdefault(kind,{}).setdefault(name,[]).append(sid) 154 | def remove_resource(self, kind, name): 155 | """Remove the resource with the given kind and name""" 156 | with self._lock: 157 | if kind in self.resources and name in self.resources[kind]: 158 | del self.resources[kind][name] 159 | self.send_multicast(tag="resource/removed",value=(kind,name)) 160 | self.send_multicast(tag="resource/{}/removed".format(kind),value=name) 161 | if kind in self._updaters and name in self._updaters[kind]: 162 | sids=self._updaters[kind].pop(name) 163 | for s in sids: 164 | self.unsubscribe(s) --------------------------------------------------------------------------------