├── .gitignore ├── LICENSE.txt ├── README.md ├── images ├── impro-function-example.png ├── opened.PNG ├── widget.png └── wiget.PNG ├── pypylon_opencv_viewer ├── __init__.py └── viewer.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # CMake 109 | cmake-build-debug/ 110 | cmake-build-release/ 111 | 112 | # Mongo Explorer plugin: 113 | .idea/**/mongoSettings.xml 114 | 115 | ## File-based project format: 116 | *.iws 117 | 118 | ## Plugin-specific files: 119 | 120 | # IntelliJ 121 | out/ 122 | 123 | # mpeltonen/sbt-idea plugin 124 | .idea_modules/ 125 | 126 | # JIRA plugin 127 | atlassian-ide-plugin.xml 128 | 129 | # Cursive Clojure plugin 130 | .idea/replstate.xml 131 | 132 | # Crashlytics plugin (for Android Studio and IntelliJ) 133 | com_crashlytics_export_strings.xml 134 | crashlytics.properties 135 | crashlytics-build.properties 136 | fabric.properties 137 | 138 | .idea/* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basler PyPylon OpenCV viewer for Jupyter Notebook 2 | 3 | [![PyPI version](https://badge.fury.io/py/pypylon-opencv-viewer.svg)](https://badge.fury.io/py/pypylon-opencv-viewer) 4 | [![Downloads](https://pepy.tech/badge/pypylon-opencv-viewer)](https://pepy.tech/project/pypylon-opencv-viewer) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | Easy to use Jupyter notebook viewer connecting Basler Pylon images grabbing with OpenCV image processing. 8 | Allows to specify interactive Jupyter widgets to manipulate Basler camera features values, grab camera image and at 9 | once get an OpenCV window on which raw camera output is displayed or you can specify an image processing function, 10 | which takes on the input raw camera output image and display your own output. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | pip install pypylon-opencv-viewer 16 | ``` 17 | 18 | ## Initialization 19 | 20 | To start working, launch Jupyter notebook and connect to Basler camera. Here is an example how we can do it: 21 | ```python 22 | from pypylon import pylon 23 | 24 | # Pypylon get camera by serial number 25 | serial_number = '22716154' 26 | info = None 27 | for i in pylon.TlFactory.GetInstance().EnumerateDevices(): 28 | if i.GetSerialNumber() == serial_number: 29 | info = i 30 | break 31 | else: 32 | print('Camera with {} serial number not found'.format(serial_number)) 33 | 34 | # VERY IMPORTANT STEP! To use Basler PyPylon OpenCV viewer you have to call .Open() method on you camera 35 | if info is not None: 36 | camera = pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateDevice(info)) 37 | camera.Open() 38 | ``` 39 | When our camera is connected and open, we can initialize our viewer with it: 40 | 41 | ```python 42 | from pypylon_opencv_viewer import BaslerOpenCVViewer 43 | viewer = BaslerOpenCVViewer(camera) 44 | ``` 45 | 46 | ### Configuration 47 | Next step is to configure created viewer using method `set_configuration`, where passed value is dictionary with the following items: 48 | 49 | features : list of dicts (required) 50 | List of widgets configuration stored in 51 | dictionaries with items: 52 | name : str (required) 53 | Camera pylon feature name, example: "GainRaw" 54 | type : str (required) 55 | widget input type, allowed values are {"int", "float", "bool", "int_text", "float_text", "choice_text"} 56 | value : number or bool (optional, default: current camera feature value) 57 | widget input value 58 | max : number (optional, default: camera feature max value) 59 | maximum widget input value, only numeric widget types 60 | min : number (optional, default: camera feature min value) 61 | minimum widget input value, only numeric widget types 62 | step : number (optional, default: camera feature increment) 63 | step of allowed input value 64 | options: list, mandatory for type "choice_text", 65 | sets values in list as options for ToggleButtons 66 | unit: str (optional, default empty) 67 | string shown at the end of label in the form "Label [unit]:" 68 | dependency: dict, (optional, default empty) 69 | defines how other widgets must be set to be this widget enabled 70 | layout : dict (optional, default: {"width": '100%', "height": '50px', "align_items": "center"}) 71 | values are passed to widget's layout 72 | style: dict, (optional, default {'description_width': 'initial'}) 73 | values are passed to widget's style 74 | 75 | Example: 76 | "features": { 77 | "name": "GainRaw", 78 | "type": "int", 79 | "value": 20, 80 | "max": 63, 81 | "min": 10, 82 | "step": 1, 83 | "layout": {"width":"99%", "height": "50px") 84 | "style": {"button_width": "90px"} 85 | } 86 | features_layout: list of tuples (optional, default is one widget per row) 87 | List of features' widgets' name for reordering. Each tuple represents one row 88 | Example: 89 | "* features_layout": [ 90 | ("Height", "Width"), 91 | ("OffsetX", "CenterX"), 92 | ("ExposureAuto", "ExposureTimeAbs"), 93 | ("AcquisitionFrameCount", "AcquisitionLineRateAbs") 94 | ], 95 | actions_layout: list of tuples (optional, default is one widget per row) 96 | List of actions' widgets' name for reordering. Each tuple represents one row. 97 | Available widgets are StatusLabel, SaveConfig, LoadConfig, ContinuousShot, SingleShot, "UserSet" 98 | * Example: 99 | "action_layout": [ 100 | ("StatusLabel"), 101 | ("SaveConfig", "LoadConfig", "ContinuousShot", "SingleShot"), 102 | ("UserSet") 103 | ] 104 | default_user_set: string (optional, default is None) 105 | If value is None, widget for selecting UserSet is displayed. 106 | Otherwise is set to given value in ["UserSet1", "UserSet2", "UserSet3"] 107 | * Example: 108 | "default_user_set": "UserSet3" 109 | 110 | The only required and also most important item in the dictionary above is a list of features you want to control. Their names can be found in [official Basler documentation](https://docs.baslerweb.com/#t=en%2Ffeatures.htm&rhsearch=sdk). 111 | 112 | Example configuration you can see below: 113 | 114 | ```python 115 | # Example of configuration for basic RGB camera's features 116 | VIEWER_CONFIG_RGB_MATRIX = { 117 | "features": [ 118 | { 119 | "name": "GainRaw", 120 | "type": "int", 121 | "step": 1, 122 | }, 123 | { 124 | "name": "Height", 125 | "type": "int", 126 | "value": 1080, 127 | "unit": "px", 128 | "step": 2, 129 | }, 130 | { 131 | "name": "Width", 132 | "type": "int", 133 | "value": 1920, 134 | "unit": "px", 135 | "step": 2, 136 | }, 137 | { 138 | "name": "CenterX", 139 | "type": "bool", 140 | }, 141 | { 142 | "name": "CenterY", 143 | "type": "bool", 144 | 145 | }, 146 | { 147 | "name": "OffsetX", 148 | "type": "int", 149 | "dependency": {"CenterX": False}, 150 | "unit": "px", 151 | "step": 2, 152 | }, 153 | { 154 | "name": "OffsetY", 155 | "type": "int", 156 | "dependency": {"CenterY": False}, 157 | "unit": "px", 158 | "step": 2, 159 | }, 160 | { 161 | "name": "AcquisitionFrameRateAbs", 162 | "type": "int", 163 | "unit": "fps", 164 | "dependency": {"AcquisitionFrameRateEnable": True}, 165 | "max": 150, 166 | "min": 1, 167 | }, 168 | { 169 | "name": "AcquisitionFrameRateEnable", 170 | "type": "bool", 171 | }, 172 | { 173 | "name": "ExposureAuto", 174 | "type": "choice_text", 175 | "options": ["Off", "Once", "Continuous"], 176 | "style": {"button_width": "90px"} 177 | }, 178 | { 179 | "name": "ExposureTimeAbs", 180 | "type": "int", 181 | "dependency": {"ExposureAuto": "Off"}, 182 | "unit": "μs", 183 | "step": 100, 184 | "max": 35000, 185 | "min": 500, 186 | }, 187 | { 188 | "name": "BalanceWhiteAuto", 189 | "type": "choice_text", 190 | "options": ["Off", "Once", "Continuous"], 191 | "style": {"button_width": "90px"} 192 | }, 193 | ], 194 | "features_layout": [ 195 | ("Height", "Width"), 196 | ("OffsetX", "CenterX"), 197 | ("OffsetY", "CenterY"), 198 | ("ExposureAuto", "ExposureTimeAbs"), 199 | ("AcquisitionFrameRateAbs", "AcquisitionFrameRateEnable"), 200 | ("BalanceWhiteAuto", "GainRaw") 201 | ], 202 | "actions_layout": [ 203 | ("StatusLabel"), 204 | ("SaveConfig", "LoadConfig", "ContinuousShot", "SingleShot"), 205 | ("UserSet") 206 | ], 207 | "default_user_set": "UserSet3", 208 | } 209 | viewer.set_configuration(VIEWER_CONFIG_RGB_MATRIX) 210 | 211 | ``` 212 | 213 | 214 | #### Image processing function 215 | We can also define image processing function that we want to apply on grabbed images using method `set_impro_function`. If we don't specify one, we will get raw camera output. 216 | 217 | The given function must either return processed image: 218 | ```python 219 | def impro(img): 220 | return np.hstack([img, (255-img)]) 221 | viewer.set_impro_function(impro) 222 | ``` 223 | or display it using cv2.namedWindow. In this case we must specify `own_window=True` to disable showing of default window. 224 | ```python 225 | def impro(img): 226 | cv2.namedWindow('1', cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL) 227 | cv2.resizeWindow('1', 1080, 720) 228 | cv2.imshow("1", np.hstack([img, (255-img)])) 229 | viewer.set_impro_function(impro, own_window=True) 230 | ``` 231 | In both cases, DON'T DESTROY ALL OpenCV windows or wait for key pressed in it! 232 | 233 | #### Viewer 234 | We have already created our viewer and set its configuration. Now we can display defined widgets using method `show_interactive_panel` 235 | with parameters `image_folder` and `window_size`. 236 | The panel contains 4 buttons: 237 | 1. Save configuration - save current values of features to camera's inner memory (UserSet) 238 | 1. Load configuration - load values of features from camera's inner memory (UserSet) to the widgets 239 | 1. Continuous shot - start streaming frames from the camera 240 | 1. Single shot - grab a one frame 241 | 242 | Also we can press 's' key to save raw camera image or impro function return value (but only when own_window=False) to `image_folder`. 243 | To close OpenCV windows just push 'q' on the keyboard. We don't have to launch this cell once more to try the same 244 | procedure with the image, just change wanted values and push the button. That's it! 245 | 246 | For configuration above we should see this interactive panel: 247 | ![Basler OpenCV viewer](images/widget.png) 248 | 249 | #### Example 250 | We can use our viewer along with more complex image processing function for detection of numbers: 251 | ```python 252 | def impro(img): 253 | img_rgb = img.copy() 254 | img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY) 255 | _, img_gray = cv2.threshold(img_gray, 170, 255, cv2.THRESH_BINARY) 256 | img_gray = 255 - img_gray 257 | _, contours, _ = cv2.findContours(img_gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 258 | selected_contours = [] 259 | for c in contours: 260 | contour_area = cv2.contourArea(c) 261 | x,y,w,h = cv2.boundingRect(c) 262 | bounding_rect_area = w*h 263 | if(contour_area > 80 and contour_area/bounding_rect_area < 0.75): 264 | selected_contours.append(c) 265 | 266 | cv2.drawContours(img_rgb, selected_contours, -1, (0,0,255), thickness=cv2.FILLED) 267 | img = cv2.putText(img, "Original", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 4, (255,0,0), 8) 268 | img_rgb = cv2.putText(img_rgb, "Found numbers", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 4, (255,0,0), 8) 269 | return np.hstack([img, img_rgb]) 270 | ``` 271 | ![Number detection](images/impro-function-example.png) 272 | 273 | #### Save or get image from camera 274 | 275 | In previous steps we set up camera features parameters using widgets. Now we can save camera image on disc or get 276 | raw openCV image (impro function return value if specified). 277 | 278 | ```python 279 | # Save image 280 | viewer.save_image('~/Documents/images/grabbed.png') 281 | 282 | # Get grabbed image 283 | img = viewer.get_image() 284 | ``` 285 | -------------------------------------------------------------------------------- /images/impro-function-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbalatsko/pypylon-opencv-viewer/b0219f54eb60634e455d067717f226bde26a7adf/images/impro-function-example.png -------------------------------------------------------------------------------- /images/opened.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbalatsko/pypylon-opencv-viewer/b0219f54eb60634e455d067717f226bde26a7adf/images/opened.PNG -------------------------------------------------------------------------------- /images/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbalatsko/pypylon-opencv-viewer/b0219f54eb60634e455d067717f226bde26a7adf/images/widget.png -------------------------------------------------------------------------------- /images/wiget.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbalatsko/pypylon-opencv-viewer/b0219f54eb60634e455d067717f226bde26a7adf/images/wiget.PNG -------------------------------------------------------------------------------- /pypylon_opencv_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .viewer import BaslerOpenCVViewer 3 | -------------------------------------------------------------------------------- /pypylon_opencv_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | import ipywidgets as widgets 4 | import warnings 5 | from pypylon.genicam import LogicalErrorException 6 | from pypylon import pylon 7 | import numpy as np 8 | import re 9 | import os 10 | import datetime 11 | 12 | 13 | class BaslerOpenCVViewer: 14 | """Easy to use Jupyter notebook interface connecting Basler Pylon images grabbing with openCV image processing. 15 | Allows to specify interactive Jupyter widgets to manipulate Basler camera features values, grab camera image and at 16 | once get an OpenCV window on which raw camera output is displayed or you can specify an image processing function, 17 | which takes on the input raw camera output image and display your own output. 18 | """ 19 | 20 | WIDGET_TYPES = { 21 | 'int': widgets.IntSlider, 22 | 'float': widgets.FloatSlider, 23 | 'bool': widgets.Checkbox, 24 | 'int_text': widgets.BoundedIntText, 25 | 'float_text': widgets.BoundedFloatText, 26 | 'choice_text': widgets.ToggleButtons, 27 | } 28 | 29 | def __init__(self, camera): 30 | """ 31 | 32 | Parameters 33 | ---------- 34 | camera : camera 35 | Basler Pylon opened camera instance. 36 | """ 37 | if not camera.IsOpen(): 38 | raise ValueError("Camera object {} is closed.".format(camera)) 39 | self._camera = camera 40 | 41 | self._interact_camera_widgets = {} 42 | self._dependecies = {} 43 | self._default_user_set = None 44 | self._disable_updates = False 45 | self._default_layout = {"width": '100%', "height": '50px', "align_items": "center"} 46 | self._default_style = {'description_width': 'initial'} 47 | self._features_layout = [] 48 | self._actions_layout = [("StatusLabel"), ("SaveConfig", "LoadConfig", "ContinuousShot", "SingleShot"), ("UserSet")] 49 | self._impro_function = None 50 | self._impro_own_window = False 51 | 52 | def set_camera(self, camera): 53 | """ Sets Basler Pylon opened camera instance. 54 | 55 | Parameters 56 | ---------- 57 | camera : camera 58 | Basler Pylon opened camera instance. 59 | 60 | Returns 61 | ------- 62 | None 63 | """ 64 | if not camera.IsOpen(): 65 | raise ValueError() 66 | self._camera = camera 67 | 68 | def set_configuration(self, configuration): 69 | """ Give configuration for creating interactive panel inside Jupyter notebook using ipywidgets to control Bastler camera 70 | 71 | Parameters 72 | ---------- 73 | configuration: dict with items: 74 | features : list of dicts (required) 75 | List of widgets configuration stored in 76 | dictionaries with items: 77 | name : str (required) 78 | Camera pylon feature name, example: "GainRaw" 79 | type : str (required) 80 | widget input type, allowed values are {"int", "float", "bool", "int_text", "float_text", "choice_text"} 81 | value : number or bool (optional, default: current camera feature value) 82 | widget input value 83 | max : number (optional, default: camera feature max value) 84 | maximum widget input value, only numeric widget types 85 | min : number (optional, default: camera feature min value) 86 | minimum widget input value, only numeric widget types 87 | step : number (optional, default: camera feature increment) 88 | step of allowed input value 89 | options: list, mandatory for type "choice_text", 90 | sets values in list as options for ToggleButtons 91 | unit: str (optional, default empty) 92 | string shown at the end of label in the form "Label [unit]:" 93 | dependency: dict, (optional, default empty) 94 | defines how other widgets must be set to be this widget enabled 95 | layout : dict (optional, default: {"width": '100%', "height": '50px', "align_items": "center"}) 96 | values are passed to widget's layout 97 | style: dict, (optional, default {'description_width': 'initial'}) 98 | values are passed to widget's style 99 | 100 | Example: 101 | "features": { 102 | "name": "GainRaw", 103 | "type": "int", 104 | "value": 20, 105 | "max": 63, 106 | "min": 10, 107 | "step": 1, 108 | "layout": {"width":"99%", "height": "50px") 109 | "style": {"button_width": "90px"} 110 | } 111 | features_layout: list of tuples (optional, default is one widget per row) 112 | List of features' widgets' name for reordering. Each tuple represents one row 113 | Example: 114 | "features_layout": [ 115 | ("Height", "Width"), 116 | ("OffsetX", "CenterX"), 117 | ("ExposureAuto", "ExposureTimeAbs"), 118 | ("AcquisitionFrameCount", "AcquisitionLineRateAbs") 119 | ], 120 | actions_layout: list of tuples (optional, default is one widget per row) 121 | List of actions' widgets' name for reordering. Each tuple represents one row. 122 | Available widgets are StatusLabel, SaveConfig, LoadConfig, ContinuousShot, SingleShot, "UserSet" 123 | Example: 124 | "action_layout": [ 125 | ("StatusLabel"), 126 | ("SaveConfig", "LoadConfig", "ContinuousShot", "SingleShot"), 127 | ("UserSet") 128 | ] 129 | default_user_set: string (optional, default is None) 130 | If value is None, widget for selecting UserSet is displayed. 131 | Otherwise is set to given value in ["UserSet1", "UserSet2", "UserSet3"] 132 | Example: 133 | "default_user_set": "UserSet3" 134 | Returns 135 | ------- 136 | None 137 | """ 138 | 139 | 140 | new_interact_camera_widgets = {} 141 | dependencies = {} 142 | if(not isinstance(configuration, dict)): 143 | raise ValueError("Given configuration must be dict type") 144 | 145 | if("features" in configuration): 146 | for feature in configuration["features"]: 147 | self._process_feature(feature, new_interact_camera_widgets, dependencies) 148 | else: 149 | warnings.warn("Configuration does not contain attribute 'features'") 150 | 151 | if("features_layout" in configuration): 152 | self._features_layout = configuration["features_layout"] 153 | if("actions_layout" in configuration): 154 | self._actions_layout = configuration["actions_layout"] 155 | if("default_user_set" in configuration): 156 | self._default_user_set = configuration["default_user_set"] 157 | 158 | self._add_user_actions_to_widgets() 159 | self._dependecies = dependencies 160 | self._interact_camera_widgets = new_interact_camera_widgets 161 | 162 | for trigger in self._dependecies.keys(): 163 | if(trigger not in self._interact_camera_widgets): 164 | raise ValueError(f"Unknown widget {trigger} listed in dependecies") 165 | self._interact_camera_widgets[trigger].observe(lambda x, trigger=trigger: self._event_handler_for_dependencies(trigger, x), names='value') 166 | self._event_handler_for_dependencies(trigger, {"new": self._interact_camera_widgets[trigger].value}) 167 | 168 | def _process_feature(self, feature, new_interact_camera_widgets, dependencies): 169 | widget_kwargs = {} 170 | 171 | if(not isinstance(feature, dict)): 172 | raise ValueError("Feature is not dict type") 173 | feature_name = feature.get('name') 174 | if(feature_name is None): 175 | raise ValueError("'name' attribute can't be None") 176 | 177 | feature_dep = feature.get('dependency', None) 178 | if(feature_dep is not None): 179 | if not isinstance(feature_dep, dict): 180 | raise ValueError("Dependency must be dict type") 181 | for f, c in feature_dep.items(): 182 | if(f not in dependencies): 183 | dependencies[f] = {} 184 | dependencies[f][feature_name] = c 185 | 186 | type_name = feature.get('type') 187 | if type_name is None: 188 | raise ValueError("'type' attribute can't be None") 189 | 190 | widget_obj = self.WIDGET_TYPES.get(type_name) 191 | if widget_obj is None: 192 | raise ValueError("Widget type name '{}' is not valid.".format(type_name)) 193 | 194 | if(type_name in ['int', 'float', 'int_text', 'float_text']): 195 | try: 196 | pylon_feature = getattr(self._camera, feature_name) 197 | except LogicalErrorException: 198 | raise ValueError("Camera doesn't have attribute '{}'".format(feature_name)) from None 199 | widget_kwargs['value'] = feature.get('value', pylon_feature.GetValue()) 200 | step = feature.get('step') 201 | if step is None: 202 | try: 203 | step = pylon_feature.GetInc().GetValue() 204 | except: 205 | step = 1 206 | widget_kwargs['step'] = step 207 | 208 | max_value = feature.get('max', pylon_feature.GetMax()) 209 | max_value = (pylon_feature.GetMax(), max_value)[max_value <= pylon_feature.GetMax()] 210 | widget_kwargs['max'] = max_value 211 | 212 | min_value = feature.get('min', pylon_feature.GetMin()) 213 | min_value = (pylon_feature.GetMin(), min_value)[min_value <= pylon_feature.GetMin()] 214 | widget_kwargs['min'] = min_value 215 | 216 | elif(type_name == "bool"): 217 | try: 218 | pylon_feature = getattr(self._camera, feature_name) 219 | except LogicalErrorException: 220 | raise ValueError("Camera doesn't have attribute '{}'".format(feature_name)) from None 221 | widget_kwargs['value'] = feature.get('value', pylon_feature.GetValue()) 222 | 223 | elif(type_name == "choice_text"): 224 | try: 225 | pylon_feature = getattr(self._camera, feature_name) 226 | except LogicalErrorException: 227 | raise ValueError("Camera doesn't have attribute '{}'".format(feature_name)) from None 228 | widget_kwargs['value'] = feature.get('value', pylon_feature.GetValue()) 229 | if('options' not in feature or not isinstance(feature['options'], list)): 230 | raise ValueError("Widget 'choice_text' has mandatory attribute 'options' (list)") 231 | elif(not feature.get('options')): 232 | raise ValueError("Attribute 'options' cannot be empty") 233 | 234 | widget_kwargs['options'] = feature.get('options') 235 | if(widget_kwargs['value'] not in widget_kwargs['options']): 236 | warnings.warn("Current value of feature '{}' is '{}', but this value is not in options.".format(feature_name, widget_kwargs['value'])) 237 | widget_kwargs['value'] = widget_kwargs['options'][0] 238 | 239 | elif(type_name == "h_box"): 240 | content = feature.get('content') 241 | if(content is None): 242 | raise ValueError("Attribute 'content' cannot be empty for type 'h_box'") 243 | for content_feature in content: 244 | self._process_feature(content_feature, new_interact_camera_widgets, dependencies) 245 | 246 | widget_kwargs['description'] = re.sub('([a-z])([A-Z])', r'\1 \2', feature_name) 247 | if('unit' in feature): 248 | widget_kwargs['description'] += " ["+feature['unit']+"]" 249 | if(type_name != "bool"): 250 | widget_kwargs['description'] += ":" 251 | 252 | style_dict = feature.get('style') 253 | if(style_dict is not None): 254 | if(not isinstance(style_dict, dict)): 255 | raise ValueError("Attribute 'style' must be dict type") 256 | else: 257 | style_dict = {} 258 | widget_kwargs['style'] = {**self._default_style, **style_dict} 259 | 260 | layout_dict = feature.get('layout') 261 | if(layout_dict is not None): 262 | if(not isinstance(layout_dict, dict)): 263 | raise ValueError("Attribute 'layout' must be dict type") 264 | else: 265 | layout_dict = {} 266 | widget_kwargs['layout'] = widgets.Layout(**{**self._default_layout, **layout_dict}) 267 | 268 | new_interact_camera_widgets[feature_name] = widget_obj(**widget_kwargs) 269 | 270 | 271 | def _event_handler_for_dependencies(self, trigger, change): 272 | for feature, condition in self._dependecies[trigger].items(): 273 | self._interact_camera_widgets[feature].disabled = (change['new'] != condition) 274 | 275 | def _button_clicked(self, button): 276 | if(button.description == "Load configuration"): 277 | self._interact_action_widgets["StatusLabel"].value = f"Status: Loading configuration from {self._camera.UserSetSelector.GetValue()}" 278 | self._camera.UserSetLoad() 279 | self._update_values_from_camera() 280 | self._interact_action_widgets["StatusLabel"].value = f"Status: Configuration was loaded from {self._camera.UserSetSelector.GetValue()}" 281 | elif(button.description == "Save configuration"): 282 | self._interact_action_widgets["StatusLabel"].value = f"Status: Saving configuration to {self._camera.UserSetSelector.GetValue()}" 283 | self._camera.UserSetSave() 284 | self._interact_action_widgets["StatusLabel"].value = f"Status: Configuration was saved to {self._camera.UserSetSelector.GetValue()}" 285 | elif(button.description == "Continuous shot"): 286 | self._run_continuous_shot(window_size=self._window_size, image_folder=self._image_folder) 287 | elif(button.description == "Single shot"): 288 | self._run_single_shot(window_size=self._window_size, image_folder=self._image_folder) 289 | 290 | def _add_user_actions_to_widgets(self): 291 | self._interact_action_widgets = {} 292 | if(self._default_user_set is None): 293 | self._interact_action_widgets["UserSet"] = widgets.ToggleButtons(options=['UserSet1', 'UserSet2', 'UserSet3'], 294 | value=self._camera.UserSetSelector.GetValue(), 295 | description='User Set', 296 | layout=widgets.Layout(**self._default_layout), 297 | style={**self._default_style, **{"button_width": "120px"}}) 298 | self._interact_action_widgets["UserSet"].observe(lambda x: self._camera.UserSetSelector.SetValue(x['new']), names='value') 299 | else: 300 | self._camera.UserSetSelector.SetValue(self._default_user_set) 301 | self._interact_action_widgets["LoadConfig"] = widgets.Button(description='Load configuration', 302 | button_style='warning', 303 | icon='cloud-upload', 304 | tooltip='Load configuration from selected UserSet', 305 | layout=widgets.Layout(**self._default_layout), 306 | style={**self._default_style, **{"button_width": "100px"}}) 307 | self._interact_action_widgets["LoadConfig"].on_click(self._button_clicked) 308 | self._interact_action_widgets["SaveConfig"] = widgets.Button(description='Save configuration', 309 | button_style='warning', 310 | icon='save', 311 | tooltip='Save configuration to selected UserSet', 312 | layout=widgets.Layout(**self._default_layout), 313 | style={**self._default_style, **{"button_width": "100px"}}) 314 | self._interact_action_widgets["SaveConfig"].on_click(self._button_clicked) 315 | self._interact_action_widgets["ContinuousShot"] = widgets.Button(description='Continuous shot', 316 | button_style='success', 317 | icon='film', 318 | tooltip='Grab and display continuous stream', 319 | layout=widgets.Layout(**self._default_layout), 320 | style={**self._default_style, **{"button_width": "100px"}}) 321 | self._interact_action_widgets["ContinuousShot"].on_click(self._button_clicked) 322 | 323 | self._interact_action_widgets["SingleShot"] = widgets.Button(description='Single shot', 324 | button_style='success', 325 | icon='image', 326 | tooltip='Grab one image and display', 327 | layout=widgets.Layout(**self._default_layout), 328 | style={**self._default_style, **{"button_width": "100px"}}) 329 | self._interact_action_widgets["SingleShot"].on_click(self._button_clicked) 330 | self._interact_action_widgets["StatusLabel"] = widgets.Label(value="Status: Connection was established", 331 | layout=widgets.Layout(**{**self._default_layout}), 332 | style=self._default_style) 333 | 334 | def set_impro_function(self, impro_function, own_window=False): 335 | """ Sets image processing function in wich grabbed image would be passed. 336 | 337 | Parameters 338 | ---------- 339 | impro_function : function 340 | Image processing function which takes one positional argument: grabbed OpenCV image. 341 | Given function must either return processed image (for default own_window=False) 342 | or display it using cv2.namedWindow (for own_window=True) 343 | own_window: bool (default False) 344 | Specify whenever impro_function opens own cv2.namedWindow 345 | 346 | Returns 347 | ------- 348 | None 349 | """ 350 | if impro_function is not None and not callable(impro_function): 351 | raise ValueError("Object {} is not callable.".format(impro_function)) 352 | self._impro_function = impro_function 353 | self._impro_own_window = own_window 354 | 355 | def _order_widgets_to_rows(self, rows, wdgts): 356 | items_rearranged = [] 357 | row_widgets = [] 358 | h_box_layout = widgets.Layout(display='flex', flex_flow='row', justify_items='center', justify_content="flex-start", width='100%') 359 | for items_in_row in rows: 360 | for item in items_in_row: 361 | if(item not in wdgts): 362 | break 363 | else: 364 | items_rearranged.extend(items_in_row) 365 | row_widgets.append(widgets.HBox([wdgts[item] for item in items_in_row], layout=h_box_layout)) 366 | 367 | return row_widgets + [w for key, w in wdgts.items() if key not in items_rearranged] 368 | 369 | def show_interactive_panel(self, window_size=None, image_folder='.'): 370 | """ Creates Jupyter notebook widgets with all specified features value controls and displays it. 371 | 372 | Parameters 373 | ---------- 374 | window_size : tuple (width, height) (optional) 375 | Size of displaying OpenCV window(raw camera output), if image processing function is not specified. 376 | image_folder : str 377 | Path to image folder to save grabbed image 378 | """ 379 | 380 | self._window_size = window_size 381 | self._image_folder = image_folder 382 | if self._camera is None or not self._camera.IsOpen(): 383 | raise ValueError("Camera object {} is closed.".format(self._camera)) 384 | 385 | row_widgets = [] 386 | row_widgets.extend(self._order_widgets_to_rows( 387 | rows=self._actions_layout, 388 | wdgts=self._interact_action_widgets)) 389 | row_widgets.extend(self._order_widgets_to_rows( 390 | rows=self._features_layout, 391 | wdgts=self._interact_camera_widgets)) 392 | 393 | ui = widgets.VBox(row_widgets, 394 | layout=widgets.Layout(display='flex', flex_flow='column', align_items='center', align_content="center", width='100%') 395 | ) 396 | w = widgets.interactive_output(self._update_values_from_widgets, {**self._interact_camera_widgets}) 397 | display(w, ui) 398 | 399 | def _update_values_from_widgets(self, **kwargs): 400 | if(not self._disable_updates): 401 | for widget_name, value in kwargs.items(): 402 | if(not self._interact_camera_widgets[widget_name].disabled): 403 | setattr(self._camera, widget_name, value) 404 | 405 | def _update_values_from_camera(self): 406 | self._disable_updates = True 407 | for widget_name, widget in self._interact_camera_widgets.items(): 408 | widget.value = getattr(self._camera, widget_name).GetValue() 409 | self._disable_updates = False 410 | 411 | def _run_continuous_shot(self, grab_strategy=pylon.GrabStrategy_LatestImageOnly, 412 | window_size=None, image_folder='.'): 413 | self._camera.StopGrabbing() 414 | 415 | # converting to opencv bgr format 416 | converter = pylon.ImageFormatConverter() 417 | converter.OutputPixelFormat = pylon.PixelType_BGR8packed 418 | converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned 419 | 420 | if(not self._impro_own_window): 421 | cv2.namedWindow('camera_image', cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL) 422 | if(window_size is not None): 423 | cv2.resizeWindow('camera_image', window_size[0], window_size[1]) 424 | 425 | self._camera.StartGrabbing(grab_strategy) 426 | try: 427 | while(self._camera.IsGrabbing()): 428 | grab_result = self._camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException) 429 | 430 | if grab_result.GrabSucceeded(): 431 | # Access the image data 432 | image = converter.Convert(grab_result) 433 | img = image.GetArray() 434 | 435 | if(self._impro_own_window): 436 | self._impro_function(img) 437 | elif(self._impro_function is not None): 438 | img = self._impro_function(img) 439 | if(not isinstance(img, np.ndarray)): 440 | cv2.destroyAllWindows() 441 | raise ValueError("The given impro_function must return a numpy array when own_window=False") 442 | cv2.imshow('camera_image', img) 443 | else: 444 | cv2.imshow('camera_image', img) 445 | k = cv2.waitKey(1) & 0xFF 446 | if(k == ord('s') and self._impro_own_window is False): 447 | path = os.path.join(image_folder, 'BaslerGrabbedImage-' + 448 | str(int(datetime.datetime.now().timestamp()))+'.png') 449 | cv2.imwrite(path, img) 450 | self._interact_action_widgets["StatusLabel"].value = f"Status: Grabbed image was saved to {path}" 451 | elif k == ord('q'): 452 | break 453 | grab_result.Release() 454 | finally: 455 | cv2.destroyAllWindows() 456 | self._camera.StopGrabbing() 457 | 458 | def _run_single_shot(self, window_size=None, image_folder='.'): 459 | self._camera.StopGrabbing() 460 | 461 | # converting to opencv bgr format 462 | converter = pylon.ImageFormatConverter() 463 | converter.OutputPixelFormat = pylon.PixelType_BGR8packed 464 | converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned 465 | 466 | if(not self._impro_own_window): 467 | cv2.namedWindow('camera_image', cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL) 468 | if(window_size is not None): 469 | cv2.resizeWindow('camera_image', window_size[0], window_size[1]) 470 | 471 | grab_result = self._camera.GrabOne(10000) 472 | if(not grab_result.GrabSucceeded()): 473 | self._interact_action_widgets["StatusLabel"].value = f"Status: The single shot action has failed." 474 | cv2.destroyAllWindows() 475 | return 476 | else: 477 | self._interact_action_widgets["StatusLabel"].value = f"Status: The single shot was successfully grabbed." 478 | image = converter.Convert(grab_result) 479 | img = image.GetArray() 480 | 481 | if(self._impro_own_window): 482 | self._impro_function(img) 483 | elif(self._impro_function is not None): 484 | img = self._impro_function(img) 485 | if(not isinstance(img, np.ndarray)): 486 | cv2.destroyAllWindows() 487 | raise ValueError("The given impro_function must return a numpy array when own_window=False") 488 | cv2.imshow('camera_image', img) 489 | else: 490 | cv2.imshow('camera_image', img) 491 | while True: 492 | k = cv2.waitKey(1) & 0xFF 493 | if(k == ord('s') and self._impro_own_window is False): 494 | path = os.path.join(image_folder, 'BaslerGrabbedImage-' + 495 | str(int(datetime.datetime.now().timestamp()))+'.png') 496 | cv2.imwrite(path, img) 497 | self._interact_action_widgets["StatusLabel"].value = f"Status: The grabbed image was saved to {path}" 498 | elif k == ord('q'): 499 | break 500 | cv2.destroyAllWindows() 501 | 502 | def save_image(self, filename): 503 | """Saves grabbed image or impro function return value, if specified 504 | 505 | Parameters 506 | ---------- 507 | filename : str 508 | Filename of grabbed image 509 | 510 | Returns 511 | ------- 512 | None 513 | """ 514 | if self._camera is None or not self._camera.IsOpen(): 515 | raise ValueError("Camera object {} is closed.".format(self._camera)) 516 | 517 | converter = pylon.ImageFormatConverter() 518 | converter.OutputPixelFormat = pylon.PixelType_BGR8packed 519 | converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned 520 | 521 | grab_result = self._camera.GrabOne(5000) 522 | image = converter.Convert(grab_result) 523 | img = image.GetArray() 524 | if self._impro_function: 525 | img = self._impro_function(img) 526 | cv2.imwrite(filename, img) 527 | 528 | def get_image(self): 529 | """Returns grabbed image or impro function return value, if specified 530 | 531 | Returns 532 | ------- 533 | openCV image 534 | """ 535 | if self._camera is None or not self._camera.IsOpen(): 536 | raise ValueError("Camera object {} is closed.".format(self._camera)) 537 | 538 | converter = pylon.ImageFormatConverter() 539 | converter.OutputPixelFormat = pylon.PixelType_BGR8packed 540 | converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned 541 | 542 | grab_result = self._camera.GrabOne(5000) 543 | image = converter.Convert(grab_result) 544 | img = image.GetArray() 545 | if self._impro_function: 546 | img = self._impro_function(img) 547 | return img 548 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jupyter==1.0.0 2 | pypylon==1.3.1 3 | ipython==6.5.0 4 | ipywidgets==7.4.2 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 5 | 6 | with open('README.md') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name='pypylon-opencv-viewer', 11 | packages=find_packages(), 12 | version='1.0.3', 13 | description='Impro function application while saving and getting image', 14 | long_description=long_description, 15 | long_description_content_type='text/markdown', 16 | license='MIT License', 17 | author='Maksym Balatsko', 18 | author_email='mbalatsko@gmail.com', 19 | url='https://github.com/mbalatsko/pypylon-opencv-viewer', 20 | download_url='https://github.com/mbalatsko/pypylon-opencv-viewer/archive/1.0.3.tar.gz', 21 | install_requires=[ 22 | 'jupyter', 23 | 'pypylon', 24 | 'ipywidgets', 25 | 'ipython' 26 | ], 27 | keywords=['basler', 'pypylon', 'opencv', 'jypyter', 'pypylon viewer', 'opencv pypylon'], 28 | classifiers=[ 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.0', 31 | 'Programming Language :: Python :: 3.1', 32 | 'Programming Language :: Python :: 3.2', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: 3.7', 38 | 'Operating System :: OS Independent' 39 | ], 40 | ) --------------------------------------------------------------------------------