├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── python-package.yml ├── .gitignore ├── .vscode ├── .ropeproject │ ├── config.py │ └── objectdb └── settings.json ├── LICENSE.txt ├── README.md ├── birbcam.sh ├── birbcam ├── __init__.py ├── __main__.py ├── birbcam.py ├── birbconfig.py ├── birbwatcher.py ├── common.py ├── exposureadjust │ ├── __init__.py │ ├── adjust.py │ ├── adjustdown.py │ ├── adjustup.py │ ├── exposureadjust.py │ ├── exposurestate.py │ ├── sleep.py │ ├── utils.py │ └── watch.py ├── focusassist.py ├── histocompare.py ├── imagemask.py ├── lensshading.py ├── optioncounter.py ├── optionflipper.py ├── picturelogger │ ├── __init__.py │ └── picturelogger.py ├── picturetaker.py ├── rectanglegrabber.py └── viewfinder.py ├── config.ini ├── requirements.txt └── tests ├── __init__.py ├── adjust_test.py ├── adjustdown_test.py ├── adjustup_test.py ├── context.py ├── optioncounter_test.py ├── optionflipper_test.py ├── picturetaker_test.py ├── rectanglegrabber_test.py ├── sleep_test.py └── watch_test.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Raspberry Pi (please complete the following information):** 27 | - OS: [e.g. Buster] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.7] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | __pycache__/ -------------------------------------------------------------------------------- /.vscode/.ropeproject/config.py: -------------------------------------------------------------------------------- 1 | # The default ``config.py`` 2 | # flake8: noqa 3 | 4 | 5 | def set_prefs(prefs): 6 | """This function is called before opening the project""" 7 | 8 | # Specify which files and folders to ignore in the project. 9 | # Changes to ignored resources are not added to the history and 10 | # VCSs. Also they are not returned in `Project.get_files()`. 11 | # Note that ``?`` and ``*`` match all characters but slashes. 12 | # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' 13 | # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' 14 | # '.svn': matches 'pkg/.svn' and all of its children 15 | # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' 16 | # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' 17 | prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', 18 | '.hg', '.svn', '_svn', '.git', '.tox'] 19 | 20 | # Specifies which files should be considered python files. It is 21 | # useful when you have scripts inside your project. Only files 22 | # ending with ``.py`` are considered to be python files by 23 | # default. 24 | # prefs['python_files'] = ['*.py'] 25 | 26 | # Custom source folders: By default rope searches the project 27 | # for finding source folders (folders that should be searched 28 | # for finding modules). You can add paths to that list. Note 29 | # that rope guesses project source folders correctly most of the 30 | # time; use this if you have any problems. 31 | # The folders should be relative to project root and use '/' for 32 | # separating folders regardless of the platform rope is running on. 33 | # 'src/my_source_folder' for instance. 34 | # prefs.add('source_folders', 'src') 35 | 36 | # You can extend python path for looking up modules 37 | # prefs.add('python_path', '~/python/') 38 | 39 | # Should rope save object information or not. 40 | prefs['save_objectdb'] = True 41 | prefs['compress_objectdb'] = False 42 | 43 | # If `True`, rope analyzes each module when it is being saved. 44 | prefs['automatic_soa'] = True 45 | # The depth of calls to follow in static object analysis 46 | prefs['soa_followed_calls'] = 0 47 | 48 | # If `False` when running modules or unit tests "dynamic object 49 | # analysis" is turned off. This makes them much faster. 50 | prefs['perform_doa'] = True 51 | 52 | # Rope can check the validity of its object DB when running. 53 | prefs['validate_objectdb'] = True 54 | 55 | # How many undos to hold? 56 | prefs['max_history_items'] = 32 57 | 58 | # Shows whether to save history across sessions. 59 | prefs['save_history'] = True 60 | prefs['compress_history'] = False 61 | 62 | # Set the number spaces used for indenting. According to 63 | # :PEP:`8`, it is best to use 4 spaces. Since most of rope's 64 | # unit-tests use 4 spaces it is more reliable, too. 65 | prefs['indent_size'] = 4 66 | 67 | # Builtin and c-extension modules that are allowed to be imported 68 | # and inspected by rope. 69 | prefs['extension_modules'] = [] 70 | 71 | # Add all standard c-extensions to extension_modules list. 72 | prefs['import_dynload_stdmods'] = True 73 | 74 | # If `True` modules with syntax errors are considered to be empty. 75 | # The default value is `False`; When `False` syntax errors raise 76 | # `rope.base.exceptions.ModuleSyntaxError` exception. 77 | prefs['ignore_syntax_errors'] = False 78 | 79 | # If `True`, rope ignores unresolvable imports. Otherwise, they 80 | # appear in the importing namespace. 81 | prefs['ignore_bad_imports'] = False 82 | 83 | # If `True`, rope will insert new module imports as 84 | # `from import ` by default. 85 | prefs['prefer_module_from_imports'] = False 86 | 87 | # If `True`, rope will transform a comma list of imports into 88 | # multiple separate import statements when organizing 89 | # imports. 90 | prefs['split_imports'] = False 91 | 92 | # If `True`, rope will remove all top-level import statements and 93 | # reinsert them at the top of the module when making changes. 94 | prefs['pull_imports_to_top'] = True 95 | 96 | # If `True`, rope will sort imports alphabetically by module name instead 97 | # of alphabetically by import statement, with from imports after normal 98 | # imports. 99 | prefs['sort_imports_alphabetically'] = False 100 | 101 | # Location of implementation of 102 | # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general 103 | # case, you don't have to change this value, unless you're an rope expert. 104 | # Change this value to inject you own implementations of interfaces 105 | # listed in module rope.base.oi.type_hinting.providers.interfaces 106 | # For example, you can add you own providers for Django Models, or disable 107 | # the search type-hinting in a class hierarchy, etc. 108 | prefs['type_hinting_factory'] = ( 109 | 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') 110 | 111 | 112 | def project_opened(project): 113 | """This function is called after opening the project""" 114 | # Do whatever you like here! 115 | -------------------------------------------------------------------------------- /.vscode/.ropeproject/objectdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdpdev/birbcam/b44c95744d81d063f12dfb2521019ff89787c45a/.vscode/.ropeproject/objectdb -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jason DuPertuis 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Version **1.0 Sparrow** is official! [Download ZIP repository here](https://github.com/jdpdev/birbcam/releases/tag/v1.0) 2 | 3 | Version **2.0 Robin** is in beta, introducing BIRBVISION to identify bird species in your pictures. [Download ZIP repository here](https://github.com/jdpdev/birbcam/releases/tag/v2.0-beta) 4 | 5 | Check out the **[Roadmap](https://github.com/jdpdev/birbcam/wiki/Roadmap)** for future plans. 6 | 7 | # birbcam 8 | 9 | A Raspberry Pi-powered, change-activated camera for watching bird feeders. 10 | 11 | Check out [Birbserver](https://github.com/jdpdev/birbserver), a web interface for your Birbcam designed to run on the same RPI. 12 | 13 | [Birbvision](https://github.com/jdpdev/birbvision) is the prototype machine learning identifier to be introduced in [Birbcam v2.0](https://github.com/jdpdev/birbcam/tree/v2.0-birbvision-dev) 14 | 15 | ## Set Up 16 | ### Hardware 17 | 18 | #### Required 19 | * Raspberry Pi - All testing has been done on a [Raspberry Pi 4 Model-B](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/) with 4GB of RAM. If you're brand new to Raspberry Pi, there are many options for kits that come with the board, the OS pre-installed on a boot SD card, and a case. 20 | * Raspberry Pi Camera 21 | * [Camera Module V2](https://www.raspberrypi.org/products/camera-module-v2/) - A small 8MP, fixed-focus camera. Lens is wide so will have to be mounted close to the feeder. Focus can be adjusted, but it is not really meant to be refocused. Good for testing, but if you stick with it you will want to upgrade to... 22 | * [HQ Camera](https://www.raspberrypi.org/products/raspberry-pi-high-quality-camera/) - 12MP, a much bigger sensor than the V2, and swappable lenses. Standard lenses come with 6mm and 16mm (28mm and 85mm equivalent on a 35mm camera) focal lengths; I find the 16mm to be a very good choice. 23 | 24 | #### Good To Have 25 | * [Adafruit HQ Camera Case](https://learn.adafruit.com/raspberry-pi-hq-camera-case) - Combines the RPI and HQ Camera in a handy package. Has to be 3D printed. The HQ camera has a integrated standard tripod screw mount. 26 | * External USB Storage - Protects the boot SD card from degredation, and provides the convience of being able to plug into another computer to work on your pictures. 27 | * Longer Lenses - [Arducam](https://www.arducam.com/product-category/lenses/) sells lenses for the HQ camera with longer focal lengths, but they require color correction that is not yet available in Birbcam. 28 | 29 | ### Environment 30 | 31 | Requires the following Python packages, available on PIP 32 | ``` 33 | picamerax 34 | opecv-python 35 | numpy 36 | imutils 37 | ``` 38 | 39 | ### config.ini 40 | 41 | Important settings are saved in the `config.ini` file; some can be overridden by CLI arguments when running the app. Defaults are provided, but some will require local configuration. 42 | 43 | * `[Saving] Directory` - Where images taken by the camera are saved. It is suggested that you save to an external drive, rather than the SD card. 44 | * `[Saving] LivePictureInterval` - Number of seconds between each live picture, used by the server. Set to 0 to disable. 45 | * `[Saving] FullPictureInterval` - Number of seconds between each full picture. A full picture is not automatically taken after this interval, rather no full pictures will be taken by triggers before the interval has expired. 46 | * `[Saving] FullPictureResolution` - The resolution of a full picture. See `config.ini` for more informating relating to camera hardware. 47 | * `[Saving] LivePictureResolution` - The resolution of a live picture. See `config.ini` for more informating relating to camera hardware. 48 | * `[Detection] Threshold` - How strong the difference between the live and reference pictures must be to register as a changed pixel. Higher is less sensitive. 49 | * `[Detection] ContourArea` - How big a detected, continuous difference region must be to trigger a full picture. This balances out noise from the `Threshold` setting. Higher is less sensitive. 50 | * `[Detection] ExposureInterval` - Number of seconds between exposure checks 51 | * `[Detection] ExposureLevel` - Target exposure level. The ideal value depends on your set up (feeder color, direct/indirect light, etc), but the default of 100 is a good starting place. 52 | * `[Detection] ExposureError` - Acceptable error +/- `ExposureLevel` 53 | * `[Debug] Enable` - Debug mode shows live images from the camera and detector. Can be toggled when running with `D` key. 54 | 55 | ## Running 56 | Run the app via the command line 57 | 58 | ```python3 birbcam.py``` 59 | 60 | ### Focus Assist 61 | The first screen that opens is a live view from the camera to help you aim the camera, and set the focus. 62 | The number in the top right is a relative, unit-less value that approximates how much of the image is in focus. Max focus corresponds to the largest value. 63 | 64 | To select a specific area to focus, you can click and drag a rectangle to zoom. To reset the zoom press `R`. 65 | 66 | To continue, press `Q`. 67 | To exit the app, press `X`. 68 | 69 | ### Detector Mask 70 | The second screen that opens is a live view from the camera to set the Detection Mask. 71 | When the camera is running, only changes to the image within the detection mask are used to trigger the taking of a picture. 72 | 73 | To select a specific area to mask, you can click and drag with the mouse. The detection area is within the yellow rectangle. 74 | 75 | To continue, press `Q`. 76 | To exit the app, press `X`. 77 | 78 | ### Camera Watcher 79 | If you are running Debug Mode, the final screen is the observing interface, which you can use to monitor the camera. There are four quadrants in the display: 80 | 81 | ![2021-02-23-12-13-53](https://user-images.githubusercontent.com/6239142/111321415-480f7900-863e-11eb-83c0-5eb3b8734c4e.jpg) 82 | 83 | - `Top Left` - The live feed from the camera. Changes that could trigger a picture will be highlighted by a green rectangle. 84 | - `Top Right` - Camera settings and exposure histogram. Camera settings can be changed with the keys marked `(x)`. The histogram plots the luminance of the image and is used to assist exposure setting. 85 | - `Bottom Left` - The difference between the live and the reference image. 86 | - `Bottom Right` - The difference image clamped to a threshold value, highlighting significant changes. 87 | 88 | The camera starts paused: it will take live pictures, but will not take full pictures until unpaused. 89 | 90 | To pause/unpause recording, press `P`. 91 | To exit the app, press `Q`. 92 | -------------------------------------------------------------------------------- /birbcam.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /home/pi/Documents/Projects/birbcam 4 | /home/pi/.virtualenvs/birbcam/bin/python3 -m birbcam &> debug/log.txt -------------------------------------------------------------------------------- /birbcam/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdpdev/birbcam/b44c95744d81d063f12dfb2521019ff89787c45a/birbcam/__init__.py -------------------------------------------------------------------------------- /birbcam/__main__.py: -------------------------------------------------------------------------------- 1 | from .birbcam import run_birbcam 2 | 3 | if __name__ == "__main__": 4 | run_birbcam() -------------------------------------------------------------------------------- /birbcam/birbcam.py: -------------------------------------------------------------------------------- 1 | from picamerax.array import PiRGBArray 2 | from picamerax import PiCamera 3 | from time import sleep 4 | #import lensshading 5 | import sys 6 | import logging 7 | from setproctitle import setproctitle 8 | 9 | from .birbconfig import BirbConfig 10 | from .focusassist import FocusAssist 11 | from .imagemask import ImageMask 12 | from .birbwatcher import BirbWatcher 13 | 14 | previewResolution = (640, 480) 15 | 16 | def run_birbcam(): 17 | setproctitle("birbcam") 18 | config = BirbConfig() 19 | 20 | if not config.logFile is None: 21 | logging.basicConfig(level=logging.INFO, filename=config.logFile, format='(%(asctime)s) %(levelname)s: %(message)s', datefmt="%H:%M:%S") 22 | else: 23 | logging.basicConfig(level=logging.INFO, format='(%(asctime)s) %(levelname)s: %(message)s', datefmt="%H:%M:%S") 24 | 25 | logging.info(f"Saving output to: {config.saveTo}") 26 | if config.noCaptureMode: logging.info("Using No Capture Mode") 27 | if config.debugMode: logging.info("Using Debug Mode") 28 | 29 | camera = PiCamera() 30 | camera.resolution = previewResolution 31 | camera.framerate = 30; 32 | camera.iso = 200 33 | rawCapture = PiRGBArray(camera, size=previewResolution) 34 | 35 | """ 36 | shading = lensshading.get_lens_shading(args.get("lensshading")) 37 | if shading != None: 38 | shading = shading.astype("uint8") 39 | print(np.shape(shading)) 40 | camera.lens_shading_table = shading 41 | """ 42 | 43 | camera.exposure_mode = 'auto' 44 | camera.awb_mode = 'auto' 45 | camera.meter_mode = 'spot' 46 | 47 | sleep(2) 48 | 49 | camera.shutter_speed = camera.exposure_speed 50 | 51 | # ************************************** 52 | # Focus assist 53 | # ************************************** 54 | focusAssist = FocusAssist() 55 | if focusAssist.run(camera) == False: sys.exit() 56 | 57 | # ************************************** 58 | # Set mask 59 | # ************************************** 60 | imageMask = ImageMask() 61 | if imageMask.run(camera) == False: sys.exit() 62 | mask = imageMask.mask 63 | 64 | # ************************************** 65 | # Capture loop 66 | # ************************************** 67 | watcher = BirbWatcher(config) 68 | watcher.run(camera, mask) -------------------------------------------------------------------------------- /birbcam/birbconfig.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | from argparse import ArgumentParser 4 | 5 | class BirbConfig: 6 | def __init__(self): 7 | self.config = configparser.ConfigParser() 8 | self.config.read(f"{os.path.dirname(os.path.realpath(__file__))}/../config.ini") 9 | 10 | self.ap = ArgumentParser() 11 | self.ap.add_argument("-f", "--file", default=None, help="path to the log file") 12 | self.ap.add_argument("-n", "--no-capture", help="do not capture photos", action='store_true') 13 | self.ap.add_argument("-s", "--save", help="picture save location", default=None) 14 | self.ap.add_argument("-ls", "--lensshading", help="which lens shading compensation to use", default=None) 15 | 16 | self.args = vars(self.ap.parse_args()) 17 | 18 | def __has_argument(self, key): 19 | return self.args.get(key) 20 | 21 | def __clean_string(self, string): 22 | if string == None: return None 23 | return string.strip("'") 24 | 25 | # ******************** 26 | # [Saving] Save location details 27 | # ******************** 28 | @property 29 | def saveTo(self) -> str: 30 | """ 31 | Root save location for images 32 | 33 | :return: Root save path 34 | :rtype: str 35 | """ 36 | arg = self.args.get("save") 37 | if arg != None: return arg 38 | 39 | return self.config["Saving"]["Directory"] 40 | 41 | @property 42 | def livePictureInterval(self) -> float: 43 | """ Number of seconds between each live picture. 44 | 45 | :returns: Number of seconds between each live picture, or 0 to disable 46 | :rtype: float 47 | """ 48 | return float(self.config["Saving"]["LivePictureInterval"]) 49 | 50 | @property 51 | def fullPictureInterval(self) -> float: 52 | """ 53 | Minimum number of seconds between each full picture. The actual elapsed time between full pictures can be longer than this if there is no picture event after the interval. 54 | 55 | :returns: Number of seconds between each full picture 56 | :rtype: float 57 | """ 58 | return float(self.config["Saving"]["LivePictureInterval"]) 59 | 60 | @property 61 | def fullPictureResolution(self) -> str: 62 | """ 63 | Resolution is a string as dimensions: x 64 | Note that the camera modules only natively supports certain resolutions, and the supported resolutions depend on the camera module version. Any other resolution will be scaled by the GPU. 65 | A list of native resolutions can be found on the camera module documentation under the '--mode' flag: https://www.raspberrypi.org/documentation/raspbian/applications/camera.md 66 | 67 | :returns: The resolution to use for full pictures 68 | :rtype: str 69 | """ 70 | return self.config["Saving"]["FullPictureResolution"] 71 | 72 | @property 73 | def livePictureResolution(self) -> str: 74 | """ 75 | Resolution is a string as dimensions: x 76 | Note that the camera modules only natively supports certain resolutions, and the supported resolutions depend on the camera module version. Any other resolution will be scaled by the GPU. 77 | A list of native resolutions can be found on the camera module documentation under the '--mode' flag: https://www.raspberrypi.org/documentation/raspbian/applications/camera.md 78 | 79 | :returns: The resolution to use for full pictures 80 | :rtype: str 81 | """ 82 | return self.config["Saving"]["LivePictureResolution"] 83 | 84 | # ******************** 85 | # [Detection] Detection parameters 86 | # ******************** 87 | @property 88 | def threshold(self) -> int: 89 | return int(self.config["Detection"]["Threshold"]) 90 | 91 | @property 92 | def contourArea(self) -> int: 93 | return int(self.config["Detection"]["ContourArea"]) 94 | 95 | @property 96 | def exposureInterval(self) -> int: 97 | return int(self.config["Detection"]["ExposureInterval"]) 98 | 99 | @property 100 | def exposureLevel(self) -> int: 101 | return int(self.config["Detection"]["ExposureLevel"]) 102 | 103 | @exposureLevel.setter 104 | def exposureLevel(self, value: int): 105 | if value < 0: 106 | value = 0 107 | elif value > 200: 108 | value = 200 109 | 110 | self.config["Detection"]["ExposureLevel"] = str(value) 111 | 112 | @property 113 | def exposureError(self) -> int: 114 | return int(self.config["Detection"]["ExposureError"]) 115 | 116 | 117 | # ******************** 118 | # [Debug] Debug details 119 | # ******************** 120 | @property 121 | def debugMode(self) -> bool: 122 | #arg = self.args.get("debug") 123 | #if arg != None: return arg 124 | 125 | return self.config["Debug"].getboolean("Enable", False) 126 | 127 | @debugMode.setter 128 | def debugMode(self, value: bool): 129 | self.config["Debug"]["Enable"] = str(value) 130 | 131 | @property 132 | def logFile(self) -> str: 133 | loc = self.config["Debug"].get("LogFile", None) 134 | if loc is None: return None 135 | 136 | return f"{os.path.dirname(os.path.realpath(__file__))}/{loc}" 137 | 138 | @property 139 | def noCaptureMode(self) -> bool: 140 | arg = self.args.get("no-capture") 141 | if arg != None: return arg 142 | 143 | return self.config["Debug"].getboolean("NoCapture", False) -------------------------------------------------------------------------------- /birbcam/birbwatcher.py: -------------------------------------------------------------------------------- 1 | from picamerax.array import PiRGBArray 2 | from picamerax import PiCamera 3 | from time import time, sleep 4 | from birbcam.common import get_mask_real_size, get_mask_coords, extract_image_region, build_histogram, compare_histograms 5 | import cv2 6 | import numpy as np 7 | import imutils 8 | import sched 9 | import logging 10 | from datetime import datetime 11 | from setproctitle import setproctitle 12 | from birbvision import ClassifyBird 13 | from .picturelogger import PictureLogger 14 | 15 | from birbcam.picturetaker import PictureTaker, filename_filestamp, filename_live_picture 16 | from .birbconfig import BirbConfig 17 | from .optionflipper import OptionFlipper 18 | from .optioncounter import OptionCounter 19 | from .exposureadjust import ExposureAdjust 20 | from .exposureadjust.utils import build_image_histogram, calculate_exposure_from_histogram 21 | 22 | PREVIEW_RES = (800, 600) 23 | 24 | shutterSpeeds = [40000, 33333, 25000, 20000, 16667, 12500, 10000, 8000, 5556, 5000, 4000, 3125, 2500, 2000, 1563, 1250, 1000, 800] 25 | shutterSpeedNames = ["25", "30", "40", "50", "60", "80", "100", "125", "180", "200", "250", "320", "400", "500", "640", "800", "1000", "1250"] 26 | isoSpeeds = [100, 200, 400, 600, 800] 27 | exposureComps = [-12, -6, 0, 6, 12] 28 | whiteBalanceModes = ["auto", "sunlight", "cloudy", "shade"] 29 | 30 | class BirbWatcher: 31 | def __init__(self, config: BirbConfig): 32 | self.config = config 33 | 34 | self.fullPictureTaker = PictureTaker( 35 | config.fullPictureResolution, 36 | config.fullPictureInterval, 37 | f"{config.saveTo}/full", 38 | filename_filestamp 39 | ) 40 | self.livePictureTaker = PictureTaker( 41 | config.livePictureResolution, 42 | config.livePictureInterval, 43 | config.saveTo, 44 | filename_live_picture 45 | ) 46 | 47 | self.shutterFlipper = OptionFlipper(shutterSpeeds, 6, shutterSpeedNames) 48 | self.isoFlipper = OptionFlipper(isoSpeeds, 1) 49 | self.exposureFlipper = OptionFlipper(exposureComps, 2) 50 | self.wbFlipper = OptionFlipper(whiteBalanceModes) 51 | self.thresholdCounter = OptionCounter(0, 255, 5, self.config.threshold) 52 | self.contourCounter = OptionCounter(0, 1500, 50, self.config.contourArea) 53 | 54 | self.exposureAdjust = ExposureAdjust( 55 | self.shutterFlipper, 56 | self.isoFlipper, 57 | interval=config.exposureInterval, 58 | targetLevel=config.exposureLevel, 59 | margin=config.exposureError 60 | ) 61 | self.pauseRecording = True 62 | 63 | self.classifier = ClassifyBird() 64 | self.pictureLogger = PictureLogger(f"{config.saveTo}/pictures.txt") 65 | 66 | def run(self, camera, mask): 67 | camera.shutter_speed = self.shutterFlipper.value 68 | camera.iso = self.isoFlipper.value 69 | camera.awb_mode = self.wbFlipper.value 70 | camera.exposure_compensation = self.exposureFlipper.value 71 | camera.resolution = PREVIEW_RES 72 | rawCapture = PiRGBArray(camera, size=PREVIEW_RES) 73 | 74 | return self.__loop(camera, rawCapture, mask) 75 | 76 | def __loop(self, camera, rawCapture, mask): 77 | average = None 78 | mask_resolution = get_mask_real_size(mask, PREVIEW_RES) 79 | mask_bounds = get_mask_coords(mask, PREVIEW_RES) 80 | 81 | for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): 82 | (now, masked, gray) = self.__take_preview(rawCapture, mask_bounds) 83 | 84 | if average is None: 85 | average = self.__initialize_average(gray) 86 | continue 87 | 88 | isCheckingExposure = self.exposureAdjust.check_exposure(camera, gray) 89 | 90 | (newAverage, frameDelta, thresh, convertAvg) = self.__process_average(average, gray) 91 | average = newAverage 92 | 93 | # detect contours and determine if any are big enough to trigger a picture 94 | contours = self.__find_contours(thresh) 95 | shouldTrigger = self.__should_trigger(contours) 96 | 97 | # take our pictures if it's time 98 | didTakeFullPicture = (False, None) 99 | classifyResults = [] 100 | self.livePictureTaker.take_picture(camera) 101 | 102 | if not self.pauseRecording and not isCheckingExposure and shouldTrigger and self.fullPictureTaker.readyForPicture: 103 | classify = self.__classify_image(now) 104 | classifyResults = classify[1] 105 | 106 | if classify[0]: 107 | #logging.info(f"[birbvision] shoot: {classifyResults[0].label} @ {classifyResults[0].confidence:.2f}") 108 | didTakeFullPicture = self.fullPictureTaker.take_picture(camera) 109 | if didTakeFullPicture[0]: 110 | thumbfile = f"{self.config.saveTo}/thumb/{didTakeFullPicture[1]}" 111 | cv2.imwrite(thumbfile, now) 112 | self.pictureLogger.log_picture(didTakeFullPicture[2], thumbfile, classifyResults, self.shutterFlipper.label, self.isoFlipper.label) 113 | else: 114 | # logging.info(f"[birbvision] ignore: {classifyResults[0].label} @ {classifyResults[0].confidence:.2f}") 115 | self.fullPictureTaker.reset_time() 116 | 117 | # visualize 118 | window = (None, None) 119 | if self.config.debugMode: 120 | window = self.__show_debug(contours, masked, now, gray, thresh, convertAvg, mask_resolution, frameDelta, didTakeFullPicture, isCheckingExposure, classifyResults) 121 | else: 122 | window = self.__show_control_console(gray, (600,400)) 123 | 124 | if window[0] != None: 125 | cv2.imshow("console", window[1]) 126 | 127 | if not self.__key_listener(camera): 128 | return False 129 | 130 | def __classify_image(self, image): 131 | classify = self.classifier.classify_image(image) 132 | results = classify.get_top_results(5) 133 | return (results[0].label != "None" and results[0].confidence > 0.30, results) 134 | 135 | def __take_preview(self, rawCapture, mask_bounds): 136 | now = rawCapture.array 137 | gray = self.__blur_and_grayscale(now) 138 | #now = imutils.resize(now, 640, 480) 139 | rawCapture.truncate(0) 140 | 141 | masked = extract_image_region(now, mask_bounds) 142 | gray = extract_image_region(gray, mask_bounds) 143 | 144 | return (now, masked, gray) 145 | 146 | 147 | def __blur_and_grayscale(self, image): 148 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 149 | gray = cv2.GaussianBlur(gray, (21, 21), 0) 150 | return gray 151 | 152 | def __initialize_average(self, gray): 153 | return gray.copy().astype('float') 154 | 155 | def __process_average(self, average, gray): 156 | # calculate delta 157 | cv2.accumulateWeighted(gray, average, 0.1) 158 | convertAvg = cv2.convertScaleAbs(average) 159 | frameDelta = cv2.absdiff(gray, convertAvg) 160 | thresh = cv2.threshold(frameDelta, self.thresholdCounter.value, 255, cv2.THRESH_BINARY)[1] 161 | thresh = cv2.dilate(thresh, None, iterations=2) 162 | 163 | return (average, frameDelta, thresh, convertAvg) 164 | 165 | def __find_contours(self, thresh): 166 | cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, 167 | cv2.CHAIN_APPROX_SIMPLE) 168 | cnts = imutils.grab_contours(cnts) 169 | return cnts 170 | 171 | def __should_trigger(self, contours): 172 | for c in contours: 173 | if cv2.contourArea(c) < self.contourCounter.value: 174 | continue 175 | 176 | return True 177 | 178 | return False 179 | 180 | def __compare_histograms(self, frame, key): 181 | (ahist, adata) = build_histogram(frame) 182 | (bhist, bdata) = build_histogram(key) 183 | compare = compare_histograms(ahist, bhist) 184 | 185 | return compare 186 | 187 | def __key_listener(self, camera): 188 | key = cv2.waitKey(1) & 0xFF 189 | 190 | if key == ord("s"): 191 | camera.shutter_speed = self.shutterFlipper.next() 192 | 193 | if key == ord("a"): 194 | camera.shutter_speed = self.shutterFlipper.previous() 195 | 196 | if key == ord("i"): 197 | camera.iso = self.isoFlipper.next() 198 | 199 | if key == ord("u"): 200 | camera.iso = self.isoFlipper.previous() 201 | 202 | if key == ord("e"): 203 | camera.exposure_compensation = self.exposureFlipper.next() 204 | 205 | if key == ord("w"): 206 | camera.exposure_compensation = self.exposureFlipper.previous() 207 | 208 | if key == ord("b"): 209 | camera.awb_mode = self.wbFlipper.next() 210 | 211 | if key == ord("v"): 212 | camera.awb_mode = self.wbFlipper.previous() 213 | 214 | if key == ord("t"): 215 | self.thresholdCounter.next() 216 | 217 | if key == ord("r"): 218 | self.thresholdCounter.previous() 219 | 220 | if key == ord("c"): 221 | self.contourCounter.next() 222 | 223 | if key == ord("x"): 224 | self.contourCounter.previous() 225 | 226 | if key == ord("="): 227 | self.config.exposureLevel += 2 228 | self.exposureAdjust.targetExposure = self.config.exposureLevel 229 | 230 | if key == ord("-"): 231 | self.config.exposureLevel -= 2 232 | self.exposureAdjust.targetExposure = self.config.exposureLevel 233 | 234 | if key == ord("p"): 235 | self.pauseRecording = not self.pauseRecording 236 | 237 | if key == ord("d"): 238 | self.config.debugMode = not self.config.debugMode 239 | 240 | if key == ord("q"): 241 | return False 242 | 243 | return True 244 | 245 | def __show_control_console(self, exposure, resolution): 246 | canvas = self.__draw_control_panel(exposure, resolution) 247 | return ('control console', canvas) 248 | 249 | def __show_debug(self, contours, masked, now, exposure, thresh, convertAvg, mask_resolution, frameDelta, didTakeFullPicture, isCheckingExposure, classifyResults): 250 | for c in contours: 251 | if cv2.contourArea(c) < self.contourCounter.value: 252 | continue 253 | 254 | (x, y, w, h) = cv2.boundingRect(c) 255 | cv2.rectangle(masked, (x, y), (x + w, y + h), (0, 255, 0), 2) 256 | 257 | convertAvg = cv2.cvtColor(convertAvg, cv2.COLOR_GRAY2BGR) 258 | frameDelta = cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR) 259 | thresh = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) 260 | 261 | histogram = self.__draw_control_panel(exposure, mask_resolution) 262 | 263 | rtop = cv2.hconcat([masked, histogram]) 264 | rbottom = cv2.hconcat([frameDelta, thresh]) 265 | quad = cv2.vconcat([rtop, rbottom]) 266 | 267 | if didTakeFullPicture[0] == True: 268 | bvdebug = np.zeros((mask_resolution[1],mask_resolution[0],3), np.uint8) 269 | 270 | y = 20 271 | yStep = 20 272 | for r in classifyResults: 273 | cv2.putText(bvdebug, f"{r.confidence:.2f}: {r.label}", (10, y), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 274 | y += yStep 275 | 276 | dtop = cv2.hconcat([masked, bvdebug]) 277 | dbottom = cv2.hconcat([frameDelta, thresh]) 278 | dquad = cv2.vconcat([dtop, dbottom]) 279 | 280 | cv2.imwrite(f"{self.config.saveTo}/debug/{didTakeFullPicture[1]}", dquad) 281 | 282 | return ('debug console', quad) 283 | 284 | def __draw_control_panel(self, exposure, mask_resolution): 285 | histogram = self.__draw_exposure_histogram(exposure, mask_resolution) 286 | 287 | cv2.putText(histogram, f"(S)hutter (A): {self.shutterFlipper.label}", (10, 20), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 288 | cv2.putText(histogram, f"(E)xposure (W): {self.exposureFlipper.label}", (10, 40), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 289 | cv2.putText(histogram, f"(I)SO (U): {self.isoFlipper.label}", (10, 60), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 290 | cv2.putText(histogram, f"W(B) (V): {self.wbFlipper.label}", (10, 80), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 291 | cv2.putText(histogram, f"(T)hreshold (R): {self.thresholdCounter.label}", (10, 100), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 292 | cv2.putText(histogram, f"(C)ontour (X): {self.contourCounter.label}", (10, 120), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 293 | 294 | if self.config.debugMode: 295 | cv2.putText(histogram, f"(D) Console", (10, 140), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 296 | else: 297 | cv2.putText(histogram, f"(D) Debug", (10, 140), cv2.FONT_HERSHEY_PLAIN, 1, (255, 255, 255), 1) 298 | 299 | if self.pauseRecording: 300 | cv2.putText(histogram, "PAUSED", (180, 30), cv2.FONT_HERSHEY_PLAIN, 2, (0, 0, 255), 2) 301 | 302 | if self.exposureAdjust.isAdjustingExposure: 303 | cv2.putText(histogram, "EXPOSURE", (180, 60), cv2.FONT_HERSHEY_PLAIN, 2, (0, 0, 255), 2) 304 | 305 | return histogram 306 | 307 | def __draw_exposure_histogram(self, now, resolution): 308 | halfHeight = int(resolution[1] / 2) 309 | blank = np.zeros((resolution[1],resolution[0],3), np.uint8) 310 | 311 | histogram = build_image_histogram(now) 312 | average = calculate_exposure_from_histogram(histogram) 313 | normalized = np.interp(histogram, (histogram.min(), histogram.max()), (0, halfHeight)) 314 | 315 | for x, y in enumerate(normalized): 316 | color = (255, 255, 255) 317 | height = resolution[1] - y 318 | 319 | if x == self.exposureAdjust.targetExposure: 320 | color = (0, 255, 0) 321 | height = resolution[1] - 255 322 | elif x == average: 323 | color = (255, 255, 0) 324 | height = resolution[1] - 255 325 | 326 | cv2.line(blank, (x, resolution[1]),(x, height), color) 327 | 328 | #compare = cv2.compareHist(key_hist, now_hist, cv2.HISTCMP_CHISQR) 329 | cv2.putText(blank,"%d" % average,(average + 5, resolution[1] - 100),cv2.FONT_HERSHEY_SIMPLEX,0.5,(255, 255, 0)) 330 | cv2.putText(blank,"+",(self.exposureAdjust.targetExposure + 5, resolution[1] - 80),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0, 255, 0)) 331 | cv2.putText(blank,"-",(self.exposureAdjust.targetExposure - 15, resolution[1] - 80),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0, 255, 0)) 332 | 333 | return blank 334 | -------------------------------------------------------------------------------- /birbcam/common.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import imutils 3 | import numpy as np 4 | 5 | def draw_mask(image, mask, resolution): 6 | (x, y, w, h) = mask 7 | 8 | maskOverlay = image.copy() 9 | 10 | left = int(resolution[0] * x) 11 | right = left + int(resolution[0] * w) 12 | top = int(resolution[1] * y) 13 | bottom = top + int(resolution[1] * h) 14 | 15 | cv2.rectangle(maskOverlay, (0,0), (resolution[0],top), (0,0,0), -1) 16 | cv2.rectangle(maskOverlay, (0,bottom), (resolution[0],resolution[1]), (0,0,0), -1) 17 | cv2.rectangle(maskOverlay, (0,0), (left,resolution[1]), (0,0,0), -1) 18 | cv2.rectangle(maskOverlay, (right,0), (resolution[0],resolution[1]), (0,0,0), -1) 19 | 20 | cv2.addWeighted(maskOverlay, 0.7, image, 0.3, 0, image) 21 | cv2.rectangle(image, (left, top), (right, bottom), (0, 255, 255), 1) 22 | 23 | def reduce_img(img, resize=None): 24 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 25 | gray = cv2.GaussianBlur(gray, (21, 21), 0) 26 | 27 | if not resize is None: 28 | gray = imutils.resize(gray, width=resize) 29 | 30 | return gray 31 | 32 | def build_histogram(a): 33 | hist = cv2.calcHist([a], [0], None, [256], [0,256]) 34 | cv2.normalize(hist, hist, 0, 255, cv2.NORM_MINMAX) 35 | data = np.int32(np.around(hist)) 36 | 37 | return (hist, data) 38 | 39 | def draw_histogram(data): 40 | blank = np.zeros((600,800,1), np.uint8) 41 | 42 | for x, y in enumerate(data): 43 | cv2.line(blank, (x * 3,600),(x * 3,600-y*2),(255,255,255)) 44 | 45 | return blank 46 | 47 | # takes the direct output of calcHist 48 | def compare_histograms(a, b): 49 | compare = cv2.compareHist(a, b, cv2.HISTCMP_CHISQR) 50 | return compare 51 | 52 | def change_mask_size(x, y, resolution): 53 | width = resolution[0] 54 | height = resolution[1] 55 | cx = int(width / 2) 56 | cy = int(height / 2) 57 | dx = cx - x 58 | dy = cy - y 59 | mx = abs(dx / width) * 2 60 | my = abs(dy / height) * 2 61 | 62 | return (mx, my) 63 | 64 | def get_mask_real_size(mask, resolution): 65 | (x, y, w, h) = mask 66 | return (int(resolution[0] * w), int(resolution[1] * h)) 67 | 68 | def get_mask_coords(mask, resolution): 69 | (x, y, w, h) = mask 70 | return { 71 | "tl": { 72 | "x": int(x * resolution[0]), 73 | "y": int(y * resolution[1]) 74 | }, 75 | "br": { 76 | "x": int(x * resolution[0]) + int(w * resolution[0]), 77 | "y": int(y * resolution[1]) + int(h * resolution[1]) 78 | } 79 | } 80 | 81 | def extract_image_region(image, bounds): 82 | return image[bounds["tl"]["y"]:bounds["br"]["y"], bounds["tl"]["x"]:bounds["br"]["x"]] 83 | 84 | def draw_aim_grid(image, resolution): 85 | x0 = 0 86 | x1 = int(resolution[0] / 3) 87 | x2 = int(resolution[0] / 3) * 2 88 | x3 = resolution[0] 89 | xc = int(resolution[0] / 2) 90 | y0 = 0 91 | y1 = int(resolution[1] / 3) 92 | y2 = int(resolution[1] / 3) * 2 93 | y3 = resolution[1] 94 | yc = int(resolution[1] / 2) 95 | w = 1 96 | c = (255, 0, 255) 97 | cc = (255, 255, 0) 98 | 99 | cv2.line(image, (x0, y1), (x3, y1), c, w) 100 | cv2.line(image, (x0, y2), (x3, y2), c, w) 101 | cv2.line(image, (x1, y0), (x1, y3), c, w) 102 | cv2.line(image, (x2, y0), (x2, y3), c, w) 103 | cv2.line(image, (x0, yc), (x3, yc), cc, w) 104 | cv2.line(image, (xc, y0), (xc, y3), cc, w) -------------------------------------------------------------------------------- /birbcam/exposureadjust/__init__.py: -------------------------------------------------------------------------------- 1 | from .exposureadjust import ExposureAdjust -------------------------------------------------------------------------------- /birbcam/exposureadjust/adjust.py: -------------------------------------------------------------------------------- 1 | from .exposurestate import ExposureState 2 | from .sleep import Sleep 3 | from .utils import calculate_exposure 4 | import time 5 | import logging 6 | 7 | class Adjust(ExposureState): 8 | def __init__(self): 9 | super().__init__() 10 | self._nextLookTime = 0 11 | self._lastExposure = None 12 | 13 | def reset_last_exposure(self): 14 | self._lastExposure = None 15 | 16 | def update(self, camera, image): 17 | if time.time() < self._nextLookTime: 18 | return 19 | 20 | exposure = calculate_exposure(image) 21 | 22 | if self.check_exposure(exposure): 23 | logging.info(f" >> Stop adjust") 24 | self.finish() 25 | else: 26 | self._isAdjusting = True 27 | self._lastExposure = exposure 28 | 29 | self.do_adjust(camera) 30 | self._nextLookTime = time.time() + 2 31 | 32 | def check_exposure(self, exposure): 33 | return True 34 | 35 | def do_adjust(self, camera): 36 | return 37 | 38 | def finish(self): 39 | self._isAdjusting = False 40 | self._changeState(Sleep(self._exposurer.sleepInterval)) -------------------------------------------------------------------------------- /birbcam/exposureadjust/adjustdown.py: -------------------------------------------------------------------------------- 1 | from .adjust import Adjust 2 | import numpy as np 3 | import logging 4 | 5 | class AdjustDown(Adjust): 6 | def setup(self): 7 | logging.info(f"[AdjustDown] take_over") 8 | 9 | def do_adjust(self, camera): 10 | if self._shutterFlipper.is_at_start: 11 | self.finish() 12 | return 13 | 14 | camera.shutter_speed = self._shutterFlipper.previous() 15 | 16 | def check_exposure(self, exposure): 17 | delta = exposure - self._targetLevel 18 | logging.info(f"[AdjustDown] {exposure}, {delta} < {self._levelMargin}, {self._lastExposure}") 19 | 20 | if self._lastExposure != None: 21 | lastDelta = self._lastExposure - self._targetLevel 22 | 23 | # stop if crossed line 24 | if np.sign(delta) != np.sign(lastDelta): 25 | return True 26 | 27 | # stop if close enough 28 | if abs(delta) < self._levelMargin: 29 | return True 30 | 31 | return False 32 | -------------------------------------------------------------------------------- /birbcam/exposureadjust/adjustup.py: -------------------------------------------------------------------------------- 1 | from .adjust import Adjust 2 | import numpy as np 3 | import logging 4 | 5 | class AdjustUp(Adjust): 6 | def setup(self): 7 | logging.info(f"[AdjustUp] take_over") 8 | 9 | def do_adjust(self, camera): 10 | if self._shutterFlipper.is_at_end: 11 | self.finish() 12 | return 13 | 14 | camera.shutter_speed = self._shutterFlipper.next() 15 | 16 | def check_exposure(self, exposure): 17 | delta = exposure - self._targetLevel 18 | logging.info(f"[AdjustUp] {exposure}, {delta} < {self._levelMargin}, {self._lastExposure}") 19 | 20 | if self._lastExposure != None: 21 | lastDelta = self._lastExposure - self._targetLevel 22 | 23 | # stop if crossed line 24 | if np.sign(delta) != np.sign(lastDelta): 25 | return True 26 | 27 | # stop if close enough 28 | if abs(delta) < self._levelMargin: 29 | return True 30 | 31 | return False 32 | -------------------------------------------------------------------------------- /birbcam/exposureadjust/exposureadjust.py: -------------------------------------------------------------------------------- 1 | from ..optionflipper import OptionFlipper 2 | from .exposurestate import ExposureState 3 | from .watch import Watch 4 | from cv2 import normalize, calcHist, cvtColor, COLOR_BGR2GRAY 5 | from time import time 6 | import numpy as np 7 | import logging 8 | 9 | class ExposureAdjust: 10 | def __init__(self, shutterFlipper: OptionFlipper, isoFlipper: OptionFlipper, interval: int = 300, targetLevel: int = 110, margin: int = 10): 11 | """ 12 | Parameters 13 | ---------- 14 | shutterFlipper : OptionFlipper 15 | isoFlipper : OptionFlipper 16 | """ 17 | self.shutterFlipper = shutterFlipper 18 | self.isoFlipper = isoFlipper 19 | self.targetLevel = targetLevel 20 | 21 | self._interval = interval 22 | self._actualMargin = margin 23 | 24 | self._currentState = None 25 | self.change_state(Watch(self._interval)) 26 | 27 | @property 28 | def isAdjustingExposure(self): 29 | return self._currentState.isAdjustingExposure if self._currentState != None else False 30 | 31 | @property 32 | def targetExposure(self): 33 | return self.targetLevel 34 | 35 | @targetExposure.setter 36 | def targetExposure(self, value): 37 | self.targetLevel = value 38 | 39 | @property 40 | def sleepInterval(self): 41 | return self._interval 42 | 43 | @property 44 | def levelError(self): 45 | return self._actualMargin 46 | 47 | def change_state(self, nextState: ExposureState): 48 | if self._currentState != None: 49 | self._currentState.release() 50 | 51 | self._currentState = nextState 52 | 53 | if self._currentState == None: 54 | self._currentState = Watch(self._interval) 55 | 56 | if self._currentState != None: 57 | self._currentState.take_over(self, 58 | self.shutterFlipper, 59 | self.isoFlipper, 60 | self.change_state, 61 | self.targetExposure, 62 | self._actualMargin 63 | ) 64 | 65 | def check_exposure(self, camera, image): 66 | self._currentState.update(camera, image) 67 | return self._currentState.isAdjustingExposure -------------------------------------------------------------------------------- /birbcam/exposureadjust/exposurestate.py: -------------------------------------------------------------------------------- 1 | from ..optionflipper import OptionFlipper 2 | 3 | class ExposureState: 4 | def __init__(self): 5 | self._shutterFlipper = None 6 | self._changeState = None 7 | self._targetLevel = None 8 | self._levelMargin = None 9 | self._isAdjusting = False 10 | 11 | def setup(self): 12 | return 13 | 14 | def take_over(self, exposurer, shutterFlipper: OptionFlipper, isoFlipper: OptionFlipper, changeState, targetLevel: int, levelMargin: int): 15 | self._exposurer = exposurer 16 | self._shutterFlipper = shutterFlipper 17 | self._isoFlipper = isoFlipper 18 | self._changeState = changeState 19 | self._targetLevel = targetLevel 20 | self._levelMargin = levelMargin 21 | self.setup() 22 | 23 | def update(self, camera, image): 24 | return 25 | 26 | def release(self): 27 | return 28 | 29 | @property 30 | def isAdjustingExposure(self): 31 | return self._isAdjusting -------------------------------------------------------------------------------- /birbcam/exposureadjust/sleep.py: -------------------------------------------------------------------------------- 1 | from .exposurestate import ExposureState 2 | from time import time 3 | import logging 4 | 5 | class Sleep(ExposureState): 6 | def __init__(self, waitTime): 7 | super().__init__() 8 | self._releaseTime = time() + waitTime 9 | logging.info(f"[Sleep] for {waitTime}") 10 | 11 | def update(self, camera, image): 12 | if time() < self._releaseTime: 13 | return 14 | 15 | self._changeState(None) 16 | -------------------------------------------------------------------------------- /birbcam/exposureadjust/utils.py: -------------------------------------------------------------------------------- 1 | from cv2 import cvtColor, calcHist, COLOR_BGR2GRAY 2 | import numpy as np 3 | 4 | def calculate_exposure(image): 5 | histogram = build_image_histogram(image) 6 | return calculate_exposure_from_histogram(histogram) 7 | 8 | def build_image_histogram(image): 9 | histogram = calcHist([image], [0], None, [256], [0,255]) 10 | data = np.int32(np.around(histogram)) 11 | 12 | return data 13 | 14 | def calculate_exposure_from_histogram(histogram): 15 | max = histogram.max() 16 | average = 0 17 | total = 0 18 | 19 | for x, y in enumerate(histogram): 20 | average += y * x 21 | total += y 22 | 23 | return int(average / total) 24 | -------------------------------------------------------------------------------- /birbcam/exposureadjust/watch.py: -------------------------------------------------------------------------------- 1 | from .exposurestate import ExposureState 2 | from .adjustup import AdjustUp 3 | from .adjustdown import AdjustDown 4 | from .utils import calculate_exposure 5 | from time import time 6 | import logging 7 | 8 | class Watch(ExposureState): 9 | def __init__(self, interval): 10 | super().__init__() 11 | self._interval = interval 12 | self.__increment_time() 13 | 14 | def setup(self): 15 | logging.info(f"[Watch] every {self._interval}") 16 | 17 | def update(self, camera, image): 18 | if time() < self._nextCheckTime: 19 | return 20 | 21 | level = calculate_exposure(image) 22 | delta = level - self._targetLevel 23 | logging.info(f"[Watch] level {level}, delta {delta}") 24 | 25 | if abs(delta) > self._levelMargin: 26 | if delta > 0: 27 | self._changeState(AdjustUp()) 28 | else: 29 | self._changeState(AdjustDown()) 30 | else: 31 | self.__increment_time() 32 | 33 | 34 | def __increment_time(self): 35 | self._nextCheckTime = time() + self._interval 36 | -------------------------------------------------------------------------------- /birbcam/focusassist.py: -------------------------------------------------------------------------------- 1 | from picamerax.array import PiRGBArray 2 | from picamerax import PiCamera 3 | from birbcam.common import draw_aim_grid 4 | import cv2 5 | 6 | from .rectanglegrabber import RectangleGrabber 7 | 8 | class FocusAssist: 9 | focusWindowName = "Focus Assist" 10 | focusWindowResolution = (800, 600) 11 | captureResolution = (3200, 2400) 12 | 13 | def __init__(self): 14 | self.focusStart = (0, 0) 15 | self.focusEnd = self.focusWindowResolution 16 | self.isDragging = False 17 | 18 | cv2.namedWindow(self.focusWindowName) 19 | 20 | def run(self, camera): 21 | self.zoomRect = RectangleGrabber( 22 | self.focusWindowName, 23 | self.focusWindowResolution, 24 | onEnd=lambda bounds: self.__set_zoom_rect(camera, bounds), 25 | preserveAspectRatio=True 26 | ) 27 | 28 | camera.resolution = self.focusWindowResolution 29 | rawCapture = PiRGBArray(camera, size=self.focusWindowResolution) 30 | 31 | keepGoing = self.__camera_loop(camera, rawCapture) 32 | 33 | cv2.destroyAllWindows() 34 | camera.zoom = (0, 0, 1, 1) 35 | 36 | return keepGoing 37 | 38 | def __camera_loop(self, camera, rawCapture): 39 | for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): 40 | 41 | image = frame.array 42 | rawCapture.truncate(0) 43 | 44 | laplacian_var = cv2.Laplacian(image, cv2.CV_64F).var() 45 | 46 | # drag rect 47 | if self.zoomRect.isDragging: 48 | (tl, br) = self.zoomRect.bounds 49 | cv2.rectangle(image, tl, br, (255, 0, 255), 2) 50 | 51 | # focus amount 52 | cv2.rectangle(image, (0,0), (120, 40), (255, 0, 255), -1) 53 | cv2.putText(image, str(int(laplacian_var)), (5,30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2) 54 | 55 | # crosshair 56 | draw_aim_grid(image, self.focusWindowResolution) 57 | 58 | cv2.imshow(self.focusWindowName, image) 59 | 60 | key = cv2.waitKey(1) & 0xFF 61 | 62 | if key == ord("r"): 63 | self.zoomRect.reset() 64 | 65 | if key == ord("q"): 66 | cv2.destroyWindow(self.focusWindowName) 67 | return True 68 | 69 | if key == ord("x"): 70 | cv2.destroyWindow(self.focusWindowName) 71 | return False 72 | 73 | def __set_zoom_rect(self, camera, bounds): 74 | (tl, br) = bounds 75 | rx = self.focusWindowResolution[0] 76 | ry = self.focusWindowResolution[1] 77 | 78 | x = tl[0] / rx 79 | y = tl[1] / ry 80 | w = (br[0] - tl[0]) / rx 81 | h = (br[1] - tl[1]) / ry 82 | camera.zoom = (x, y, w, h) -------------------------------------------------------------------------------- /birbcam/histocompare.py: -------------------------------------------------------------------------------- 1 | from common import draw_mask 2 | from time import time, sleep 3 | import cv2 4 | import numpy as np 5 | import imutils 6 | import argparse 7 | 8 | def reduce_img(img): 9 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 10 | gray = cv2.GaussianBlur(gray, (21, 21), 0) 11 | gray = imutils.resize(gray, width=800) 12 | return gray 13 | 14 | def build_histogram(a): 15 | hist = cv2.calcHist([a], [0], None, [256], [0,256]) 16 | cv2.normalize(hist, hist, 0, 255, cv2.NORM_MINMAX) 17 | data = np.int32(np.around(hist)) 18 | 19 | return (hist, data) 20 | 21 | def draw_histogram(data): 22 | blank = np.zeros((600,800,1), np.uint8) 23 | 24 | for x, y in enumerate(data): 25 | cv2.line(blank, (x * 3,600),(x * 3,600-y*2),(255,255,255)) 26 | 27 | return blank 28 | 29 | def compare_histograms(a, b): 30 | compare = cv2.compareHist(a, b, cv2.HISTCMP_CHISQR) 31 | return compare 32 | 33 | aimg = cv2.imread("/home/pi/Public/birbs/2021-01-22-11:02:44.jpg") 34 | bimg = cv2.imread("/home/pi/Public/birbs/2021-01-22-11:02:33.jpg") 35 | cimg = cv2.imread("/home/pi/Public/birbs/2021-01-22-11:02:21.jpg") 36 | 37 | aimg = reduce_img(aimg) 38 | bimg = reduce_img(bimg) 39 | cimg = reduce_img(cimg) 40 | delta = cv2.absdiff(aimg, cimg) 41 | 42 | (ahist, adata) = build_histogram(aimg) 43 | (bhist, bdata) = build_histogram(cimg) 44 | ahistimg = draw_histogram(adata) 45 | bhistimg = draw_histogram(bdata) 46 | comparison = compare_histograms(ahist, bhist) 47 | 48 | blank = np.zeros((600,800,1), np.uint8) 49 | 50 | left = cv2.hconcat([ahistimg, bhistimg]) 51 | left = cv2.resize(left, (800, 300)) 52 | delta = cv2.resize(delta, (400, 300)) 53 | #quad = cv2.hconcat([left, right]) 54 | #quad = cv2.resize(quad, (800, 600)) 55 | 56 | #cv2.imshow('blank', delta) 57 | #cv2.imshow('breakdown', quad) 58 | #cv2.imshow('ahist', ahistimg) 59 | #cv2.imshow('bhist', bhistimg) 60 | cv2.imshow('histograms', left) 61 | cv2.imshow('delta', delta) 62 | print("Comparison: ", comparison) 63 | 64 | while True: 65 | key = cv2.waitKey(1) & 0xFF 66 | 67 | if key == ord("q"): 68 | break 69 | 70 | cv2.destroyAllWindows() -------------------------------------------------------------------------------- /birbcam/imagemask.py: -------------------------------------------------------------------------------- 1 | from picamerax.array import PiRGBArray 2 | from picamerax import PiCamera 3 | from birbcam.common import draw_aim_grid, draw_mask 4 | import cv2 5 | 6 | from .rectanglegrabber import RectangleGrabber 7 | 8 | class ImageMask: 9 | maskWindowName = "Set Detection Region" 10 | maskWindowResolution = (800, 600) 11 | 12 | def __init__(self): 13 | self._mask = (0.25, 0.25, 0.5, 0.5) 14 | 15 | @property 16 | def mask(self): 17 | """The region to mask""" 18 | return self._mask 19 | 20 | def run(self, camera): 21 | cv2.namedWindow(self.maskWindowName) 22 | self.maskRect = RectangleGrabber( 23 | self.maskWindowName, 24 | self.maskWindowResolution, 25 | onDrag = lambda bounds: self.__set_mask_rect(bounds), 26 | onEnd = lambda bounds: self.__set_mask_rect(bounds) 27 | ) 28 | 29 | camera.resolution = self.maskWindowResolution 30 | rawCapture = PiRGBArray(camera, size=self.maskWindowResolution) 31 | 32 | keepGoing = self.__loop(camera, rawCapture) 33 | cv2.destroyAllWindows() 34 | 35 | return keepGoing 36 | 37 | def __loop(self, camera, rawCapture): 38 | for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): 39 | 40 | image = frame.array 41 | draw_mask(image, self._mask, self.maskWindowResolution) 42 | draw_aim_grid(image, self.maskWindowResolution) 43 | rawCapture.truncate(0) 44 | 45 | cv2.imshow(self.maskWindowName, image) 46 | 47 | key = cv2.waitKey(1) & 0xFF 48 | 49 | if key == ord("q"): 50 | return True 51 | 52 | if key == ord("x"): 53 | return False 54 | 55 | def __set_mask_rect(self, bounds): 56 | (tl, br) = bounds 57 | rx = self.maskWindowResolution[0] 58 | ry = self.maskWindowResolution[1] 59 | 60 | x = tl[0] / rx 61 | y = tl[1] / ry 62 | w = (br[0] - tl[0]) / rx 63 | h = (br[1] - tl[1]) / ry 64 | 65 | self._mask = (x, y, w, h) -------------------------------------------------------------------------------- /birbcam/lensshading.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | arducam_cs = np.array( 4 | [ 5 | [ 6 | [38, 36, 38, 35, 36, 36, 35, 35, 34, 34, 34, 34, 34, 34, 34, 33, 34, 33, 33, 33, 34, 34, 32, 33, 33, 33, 33, 33, 34, 33, 32, 34, 33, 33, 34, 34, 33, 35, 34, 35, 35, 34, 34, 35, 35, 36, 36, 36, 36, 39, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 7 | [36, 36, 37, 35, 34, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 33, 33, 34, 33, 34, 33, 32, 32, 33, 32, 33, 33, 32, 32, 34, 33, 33, 33, 32, 33, 33, 33, 34, 34, 33, 33, 35, 34, 35, 35, 35, 35, 36, 37, 38, 36, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 8 | [38, 36, 35, 37, 34, 35, 35, 34, 34, 35, 33, 33, 34, 33, 33, 32, 32, 32, 33, 33, 33, 33, 32, 33, 32, 32, 32, 32, 32, 33, 33, 33, 32, 33, 32, 33, 33, 33, 34, 34, 34, 34, 34, 33, 35, 34, 35, 36, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 9 | [36, 35, 35, 36, 35, 34, 35, 33, 34, 35, 34, 33, 34, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 33, 32, 33, 33, 33, 32, 33, 32, 32, 33, 33, 34, 33, 33, 33, 34, 33, 33, 35, 34, 33, 34, 35, 35, 35, 35, 36, 37, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36], 10 | [36, 36, 35, 34, 35, 34, 35, 33, 33, 33, 34, 33, 32, 33, 33, 33, 32, 33, 32, 32, 32, 33, 33, 32, 32, 32, 32, 33, 33, 33, 34, 32, 33, 33, 33, 33, 33, 33, 35, 34, 34, 33, 35, 33, 34, 34, 34, 35, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36], 11 | [35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 33, 33, 33, 32, 33, 33, 33, 32, 32, 32, 33, 32, 33, 33, 32, 33, 33, 32, 33, 32, 33, 33, 33, 32, 33, 33, 33, 33, 32, 33, 32, 33, 34, 34, 34, 34, 35, 35, 35, 34, 35, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 12 | [36, 36, 35, 34, 34, 34, 35, 33, 33, 33, 32, 33, 33, 32, 32, 33, 33, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 33, 32, 32, 32, 33, 33, 33, 33, 32, 33, 32, 32, 32, 34, 33, 34, 34, 34, 34, 35, 35, 34, 35, 34, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 13 | [36, 35, 35, 34, 34, 34, 34, 34, 34, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 32, 32, 33, 32, 32, 32, 33, 33, 32, 33, 33, 32, 32, 33, 33, 33, 33, 33, 33, 34, 33, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 14 | [35, 35, 35, 34, 34, 33, 33, 33, 33, 33, 32, 33, 32, 32, 32, 33, 33, 32, 32, 32, 32, 33, 32, 32, 33, 33, 32, 33, 33, 32, 33, 33, 33, 33, 32, 32, 33, 33, 32, 32, 33, 33, 32, 34, 34, 33, 34, 34, 35, 35, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 15 | [35, 36, 35, 35, 34, 33, 33, 33, 33, 33, 33, 32, 33, 33, 32, 32, 32, 32, 33, 32, 33, 33, 32, 33, 32, 33, 33, 32, 33, 32, 32, 33, 32, 33, 33, 33, 32, 32, 32, 33, 33, 32, 33, 33, 33, 33, 34, 34, 33, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 16 | [34, 34, 35, 35, 34, 33, 34, 33, 32, 32, 33, 32, 33, 32, 32, 33, 32, 32, 32, 33, 32, 33, 32, 32, 32, 33, 32, 32, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 32, 33, 33, 33, 34, 34, 34, 34, 35, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 17 | [35, 34, 35, 35, 34, 34, 33, 34, 34, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 33, 32, 32, 32, 32, 33, 32, 32, 33, 32, 33, 32, 32, 32, 32, 32, 32, 33, 32, 33, 33, 34, 33, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 18 | [36, 34, 35, 36, 33, 33, 33, 32, 32, 33, 33, 32, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 32, 33, 32, 32, 33, 32, 32, 32, 33, 33, 33, 33, 33, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 19 | [35, 35, 34, 35, 33, 33, 34, 33, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 32, 32, 33, 32, 32, 32, 33, 33, 32, 32, 33, 32, 32, 32, 32, 32, 33, 33, 32, 33, 32, 32, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 20 | [36, 35, 34, 34, 34, 34, 34, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 33, 32, 33, 33, 32, 32, 32, 32, 34, 32, 33, 32, 32, 33, 33, 33, 33, 34, 34, 33, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 21 | [36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 33, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 22 | [35, 35, 35, 34, 34, 33, 34, 32, 32, 32, 32, 32, 33, 32, 33, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 32, 32, 33, 32, 33, 32, 33, 33, 33, 33, 32, 33, 32, 33, 32, 33, 33, 32, 34, 34, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36], 23 | [34, 34, 35, 35, 35, 33, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 24 | [36, 35, 35, 34, 33, 34, 34, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 33, 33, 33, 32, 33, 32, 33, 33, 34, 32, 34, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 25 | [35, 35, 34, 33, 34, 33, 32, 32, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 26 | [35, 35, 35, 34, 33, 33, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 27 | [36, 34, 35, 35, 34, 34, 33, 33, 32, 32, 33, 32, 32, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 33, 34, 33, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 28 | [35, 36, 35, 34, 35, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 29 | [36, 35, 34, 34, 34, 33, 34, 34, 33, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 32, 34, 34, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33], 30 | [36, 34, 34, 34, 33, 33, 34, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 32, 32, 32, 32, 33, 33, 33, 32, 32, 32, 33, 33, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 31 | [34, 35, 34, 34, 34, 33, 33, 33, 34, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 33, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 32 | [36, 35, 34, 35, 34, 34, 34, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 32, 33, 32, 32, 32, 32, 33, 32, 32, 33, 32, 33, 33, 33, 33, 33, 33, 33, 34, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 33 | [35, 35, 34, 34, 34, 33, 33, 32, 33, 32, 32, 33, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 32, 33, 32, 32, 32, 33, 33, 32, 32, 33, 33, 34, 33, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 34 | [35, 34, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 32, 32, 33, 33, 32, 32, 32, 32, 32, 33, 32, 34, 33, 32, 32, 33, 34, 33, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], 35 | [36, 35, 34, 34, 35, 34, 33, 33, 33, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 33, 32, 33, 33, 33, 33, 34, 35, 33, 36, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 36 | [36, 35, 34, 34, 34, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 32, 33, 32, 33, 33, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 37 | [36, 36, 36, 35, 33, 34, 33, 33, 32, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 33, 32, 33, 33, 33, 34, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35], 38 | [36, 36, 34, 35, 35, 35, 34, 34, 34, 32, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 33, 32, 33, 32, 33, 33, 32, 33, 33, 35, 33, 34, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36], 39 | [35, 37, 35, 35, 35, 34, 33, 34, 33, 34, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 32, 32, 32, 32, 32, 32, 33, 32, 33, 33, 32, 33, 33, 33, 33, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36], 40 | [36, 35, 35, 36, 35, 34, 34, 33, 34, 34, 32, 32, 33, 32, 32, 32, 33, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 32, 32, 33, 32, 32, 34, 35, 34, 35, 36, 35, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 41 | [36, 36, 35, 36, 36, 34, 34, 34, 34, 34, 33, 33, 32, 33, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 32, 32, 33, 33, 33, 34, 33, 33, 33, 34, 35, 34, 34, 35, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 42 | [37, 36, 36, 35, 35, 34, 35, 34, 35, 34, 34, 33, 32, 33, 32, 32, 33, 33, 32, 32, 33, 32, 32, 33, 32, 32, 33, 33, 32, 32, 32, 32, 33, 33, 33, 33, 32, 33, 33, 33, 33, 33, 33, 34, 35, 35, 35, 34, 36, 36, 35, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 43 | [38, 36, 36, 37, 35, 35, 35, 34, 34, 33, 34, 34, 33, 34, 32, 33, 33, 34, 33, 33, 33, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 37, 36, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 44 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 45 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 46 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 47 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 48 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 49 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 50 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 51 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 52 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 53 | [39, 38, 36, 35, 35, 35, 35, 34, 35, 34, 34, 34, 33, 34, 34, 32, 33, 32, 34, 34, 34, 33, 32, 33, 33, 32, 33, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 54 | ], 55 | [ 56 | [47, 47, 48, 47, 47, 47, 48, 47, 47, 48, 47, 47, 46, 47, 46, 44, 43, 46, 44, 44, 43, 42, 42, 41, 42, 42, 43, 42, 43, 42, 42, 43, 42, 43, 42, 44, 43, 45, 44, 45, 45, 45, 46, 45, 45, 45, 46, 45, 46, 45, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 57 | [46, 47, 47, 47, 47, 48, 49, 48, 48, 46, 47, 46, 46, 46, 43, 44, 44, 43, 44, 43, 42, 42, 42, 41, 42, 41, 41, 41, 40, 42, 40, 41, 42, 43, 43, 43, 43, 45, 44, 44, 45, 45, 46, 46, 46, 45, 46, 45, 46, 45, 44, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 58 | [46, 49, 48, 48, 49, 48, 48, 49, 49, 48, 47, 47, 46, 44, 45, 43, 44, 43, 42, 42, 41, 41, 40, 41, 40, 42, 42, 40, 40, 41, 40, 41, 41, 42, 41, 43, 44, 43, 43, 43, 44, 45, 47, 44, 46, 46, 46, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 59 | [47, 48, 50, 49, 50, 50, 47, 48, 47, 48, 47, 46, 44, 44, 45, 42, 42, 42, 41, 40, 40, 40, 40, 40, 40, 40, 41, 40, 41, 39, 40, 40, 40, 40, 41, 41, 42, 43, 42, 42, 42, 44, 44, 44, 45, 46, 46, 46, 44, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 60 | [48, 50, 49, 49, 50, 49, 48, 48, 47, 46, 46, 45, 46, 43, 43, 42, 41, 40, 41, 39, 40, 39, 40, 38, 39, 39, 39, 39, 39, 38, 38, 38, 39, 40, 39, 40, 41, 41, 42, 42, 43, 44, 45, 44, 45, 46, 44, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 61 | [49, 48, 51, 49, 48, 48, 48, 47, 48, 47, 45, 44, 44, 44, 42, 40, 42, 39, 40, 38, 39, 40, 37, 38, 39, 38, 37, 37, 37, 39, 38, 38, 40, 38, 39, 39, 40, 41, 42, 42, 42, 45, 43, 44, 45, 46, 46, 45, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 62 | [48, 50, 48, 50, 51, 48, 48, 46, 47, 47, 45, 43, 43, 43, 40, 41, 40, 40, 40, 39, 38, 38, 38, 37, 37, 38, 38, 37, 37, 38, 38, 37, 38, 39, 39, 39, 40, 40, 42, 41, 41, 42, 42, 46, 46, 46, 46, 45, 48, 45, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 63 | [50, 50, 51, 50, 49, 50, 49, 47, 45, 45, 45, 43, 42, 42, 41, 41, 39, 38, 39, 38, 38, 37, 37, 37, 36, 37, 37, 37, 37, 36, 37, 37, 37, 38, 39, 37, 39, 39, 41, 40, 42, 42, 43, 44, 45, 45, 47, 45, 45, 46, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 64 | [51, 51, 49, 50, 49, 49, 47, 47, 46, 46, 45, 42, 43, 41, 40, 40, 40, 37, 38, 38, 37, 37, 36, 37, 36, 36, 36, 35, 36, 37, 36, 36, 37, 37, 37, 39, 39, 39, 39, 40, 41, 42, 43, 43, 44, 45, 47, 46, 47, 48, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 65 | [51, 52, 52, 49, 48, 50, 47, 46, 46, 45, 44, 43, 42, 40, 41, 39, 38, 39, 38, 36, 36, 37, 36, 37, 35, 35, 36, 35, 34, 36, 36, 35, 35, 37, 37, 38, 39, 38, 40, 40, 41, 43, 42, 42, 44, 45, 45, 45, 46, 46, 47, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49], 66 | [50, 52, 50, 48, 49, 49, 48, 47, 46, 44, 43, 42, 41, 40, 38, 39, 39, 38, 37, 37, 36, 35, 35, 34, 34, 35, 35, 35, 36, 36, 35, 35, 35, 37, 37, 37, 38, 38, 38, 40, 40, 41, 42, 43, 43, 44, 46, 47, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 67 | [53, 50, 52, 49, 49, 46, 47, 45, 44, 43, 42, 42, 41, 40, 39, 38, 36, 37, 37, 35, 36, 35, 35, 34, 33, 35, 34, 34, 34, 34, 35, 34, 35, 35, 35, 36, 36, 37, 38, 38, 40, 41, 43, 43, 43, 44, 44, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 68 | [51, 50, 50, 48, 48, 48, 46, 46, 45, 44, 43, 40, 40, 40, 37, 38, 37, 36, 36, 35, 35, 35, 33, 34, 35, 34, 33, 33, 34, 34, 33, 34, 36, 36, 35, 36, 36, 37, 38, 39, 40, 41, 41, 43, 44, 45, 44, 48, 45, 48, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 69 | [52, 49, 50, 50, 48, 48, 46, 46, 44, 43, 42, 40, 40, 39, 37, 38, 36, 36, 36, 36, 34, 35, 33, 34, 33, 33, 33, 33, 34, 33, 34, 34, 34, 34, 36, 36, 36, 38, 38, 39, 40, 39, 40, 42, 42, 44, 44, 45, 45, 47, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 70 | [52, 51, 51, 47, 49, 49, 46, 46, 44, 44, 43, 41, 40, 39, 38, 36, 36, 34, 35, 34, 35, 33, 33, 32, 34, 32, 33, 33, 32, 33, 33, 34, 34, 34, 34, 36, 36, 37, 37, 37, 39, 40, 40, 41, 42, 43, 44, 46, 45, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 71 | [50, 49, 51, 50, 48, 48, 46, 45, 44, 43, 41, 41, 39, 38, 37, 36, 36, 35, 35, 34, 33, 33, 33, 32, 33, 32, 33, 32, 33, 32, 33, 34, 34, 34, 35, 36, 36, 36, 36, 38, 39, 40, 40, 41, 42, 45, 45, 44, 46, 46, 46, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 72 | [52, 50, 51, 50, 46, 48, 46, 46, 44, 42, 40, 40, 39, 38, 38, 36, 35, 35, 34, 34, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 35, 35, 35, 36, 37, 37, 37, 40, 40, 41, 43, 44, 45, 45, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 73 | [51, 51, 50, 48, 48, 48, 46, 46, 44, 42, 41, 39, 39, 39, 38, 37, 36, 35, 34, 34, 33, 32, 33, 32, 32, 32, 33, 32, 33, 33, 32, 34, 33, 34, 34, 35, 35, 36, 36, 37, 39, 39, 41, 41, 43, 44, 45, 44, 45, 47, 47, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49], 74 | [50, 51, 51, 48, 48, 48, 46, 46, 44, 42, 42, 40, 39, 37, 37, 36, 36, 35, 34, 33, 33, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 34, 33, 34, 34, 35, 36, 36, 37, 38, 39, 39, 40, 41, 42, 42, 44, 44, 45, 45, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 75 | [52, 50, 50, 48, 48, 47, 46, 45, 42, 42, 42, 40, 39, 38, 36, 37, 36, 35, 34, 34, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 36, 37, 38, 39, 40, 40, 41, 42, 43, 44, 43, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 76 | [52, 50, 51, 49, 49, 48, 47, 45, 44, 43, 41, 40, 39, 38, 37, 36, 37, 34, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 35, 35, 36, 36, 37, 38, 39, 40, 41, 42, 43, 45, 45, 45, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 77 | [52, 49, 50, 47, 49, 47, 47, 45, 44, 42, 40, 40, 39, 37, 37, 36, 35, 35, 34, 34, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 33, 35, 34, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 45, 46, 46, 46, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 78 | [51, 50, 50, 48, 48, 48, 45, 46, 44, 43, 42, 41, 39, 38, 37, 36, 35, 34, 34, 34, 33, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 33, 33, 34, 35, 35, 36, 37, 37, 39, 39, 40, 41, 42, 42, 46, 46, 45, 47, 46, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 79 | [51, 49, 50, 48, 47, 47, 47, 46, 44, 42, 43, 40, 39, 38, 37, 37, 36, 35, 34, 34, 33, 32, 33, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 36, 36, 36, 36, 39, 40, 38, 41, 40, 41, 44, 45, 44, 46, 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 80 | [50, 50, 50, 49, 49, 48, 45, 45, 44, 42, 42, 40, 39, 39, 37, 36, 37, 35, 34, 34, 34, 33, 32, 32, 32, 32, 32, 33, 33, 32, 33, 34, 34, 34, 34, 34, 36, 36, 37, 38, 39, 40, 40, 41, 43, 42, 45, 46, 45, 47, 46, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 81 | [50, 49, 52, 49, 49, 47, 46, 45, 44, 42, 42, 41, 39, 39, 37, 36, 36, 35, 35, 34, 34, 33, 33, 32, 32, 32, 33, 33, 32, 33, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 42, 41, 43, 43, 45, 45, 47, 46, 46, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 82 | [52, 50, 49, 50, 47, 48, 46, 45, 44, 42, 41, 41, 39, 39, 38, 37, 35, 36, 35, 36, 34, 33, 33, 33, 33, 33, 32, 33, 33, 34, 34, 33, 35, 34, 35, 36, 35, 37, 37, 38, 39, 41, 41, 42, 43, 44, 44, 44, 46, 45, 46, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 83 | [51, 50, 49, 49, 49, 50, 46, 45, 45, 45, 43, 40, 40, 39, 39, 38, 37, 36, 35, 35, 34, 33, 34, 32, 33, 33, 34, 34, 33, 34, 34, 34, 34, 35, 35, 36, 37, 37, 38, 39, 40, 40, 41, 42, 43, 43, 45, 45, 44, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 84 | [51, 50, 49, 49, 48, 48, 46, 47, 45, 45, 42, 42, 40, 39, 39, 38, 36, 37, 35, 35, 35, 35, 35, 34, 35, 34, 33, 34, 34, 35, 34, 34, 35, 36, 35, 37, 37, 38, 37, 39, 39, 41, 42, 42, 43, 44, 47, 46, 45, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 85 | [51, 50, 50, 50, 48, 47, 47, 44, 46, 44, 44, 41, 41, 41, 39, 38, 38, 37, 36, 36, 35, 35, 35, 36, 34, 34, 35, 34, 34, 36, 36, 35, 36, 36, 37, 36, 38, 38, 39, 39, 41, 41, 42, 42, 42, 43, 44, 46, 46, 45, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 86 | [50, 49, 49, 50, 48, 47, 49, 47, 45, 45, 42, 43, 41, 41, 40, 38, 38, 38, 37, 37, 36, 36, 35, 36, 34, 35, 35, 35, 35, 35, 36, 36, 35, 37, 37, 37, 38, 39, 39, 40, 40, 41, 42, 42, 43, 45, 44, 45, 46, 45, 45, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 87 | [48, 50, 49, 50, 49, 47, 47, 48, 46, 45, 43, 43, 42, 42, 40, 41, 39, 38, 38, 37, 36, 36, 36, 36, 37, 36, 36, 35, 35, 36, 36, 36, 37, 37, 37, 38, 39, 38, 38, 40, 41, 41, 43, 42, 44, 44, 44, 46, 45, 46, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 88 | [50, 48, 49, 50, 49, 48, 48, 48, 46, 44, 45, 43, 42, 42, 41, 40, 41, 39, 39, 39, 38, 37, 37, 38, 37, 36, 37, 36, 36, 36, 37, 37, 37, 38, 38, 39, 39, 40, 41, 40, 41, 42, 43, 42, 45, 44, 46, 46, 46, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 89 | [48, 48, 50, 50, 46, 49, 49, 48, 47, 47, 44, 45, 42, 42, 42, 41, 42, 39, 40, 39, 39, 38, 37, 38, 37, 38, 37, 37, 37, 37, 39, 38, 39, 39, 38, 40, 41, 40, 40, 41, 41, 42, 44, 43, 45, 45, 44, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 90 | [48, 47, 47, 47, 49, 48, 48, 49, 46, 47, 45, 45, 43, 43, 42, 41, 41, 41, 39, 39, 38, 38, 39, 38, 38, 38, 38, 38, 37, 38, 37, 39, 39, 40, 39, 40, 40, 40, 41, 41, 42, 43, 44, 44, 45, 45, 46, 44, 45, 46, 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 91 | [47, 47, 49, 48, 48, 49, 48, 46, 48, 47, 45, 45, 43, 46, 43, 42, 42, 42, 39, 40, 40, 40, 39, 39, 39, 38, 38, 38, 40, 39, 39, 40, 40, 40, 40, 40, 41, 40, 41, 42, 43, 43, 43, 44, 45, 45, 44, 45, 47, 47, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 92 | [46, 47, 48, 47, 49, 47, 47, 49, 48, 46, 45, 46, 44, 43, 43, 42, 42, 41, 42, 41, 40, 40, 40, 40, 40, 40, 39, 39, 40, 39, 39, 40, 40, 40, 40, 40, 41, 42, 42, 42, 42, 43, 43, 43, 43, 46, 46, 45, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 93 | [44, 47, 48, 48, 47, 47, 49, 47, 46, 47, 47, 46, 45, 45, 43, 43, 42, 42, 42, 42, 41, 41, 40, 40, 40, 39, 40, 40, 40, 40, 39, 40, 40, 40, 41, 42, 42, 42, 42, 43, 42, 43, 44, 45, 46, 45, 44, 44, 44, 45, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 94 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 95 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 96 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 97 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 98 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 99 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 100 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 101 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 102 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 103 | [46, 46, 47, 47, 47, 48, 48, 48, 47, 49, 48, 48, 45, 45, 45, 44, 43, 43, 42, 43, 43, 43, 42, 42, 42, 41, 41, 43, 43, 41, 41, 42, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 46, 45, 46, 46, 45, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 104 | ], 105 | [ 106 | [45, 44, 46, 45, 47, 47, 47, 48, 48, 48, 48, 47, 48, 49, 45, 47, 47, 46, 46, 46, 46, 45, 47, 44, 46, 45, 45, 45, 45, 45, 47, 46, 46, 46, 46, 45, 47, 47, 47, 46, 47, 48, 48, 47, 46, 46, 46, 45, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 107 | [46, 45, 45, 48, 47, 47, 48, 48, 47, 46, 47, 48, 48, 47, 45, 46, 45, 46, 46, 44, 44, 45, 44, 43, 45, 44, 44, 45, 44, 44, 44, 44, 44, 44, 44, 45, 45, 46, 46, 46, 46, 45, 47, 46, 48, 46, 46, 47, 45, 47, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 108 | [46, 45, 46, 47, 46, 48, 47, 47, 47, 47, 47, 47, 47, 47, 45, 44, 45, 43, 44, 44, 43, 43, 43, 42, 42, 43, 43, 43, 43, 42, 45, 44, 43, 43, 44, 44, 45, 45, 44, 45, 45, 46, 46, 46, 47, 46, 47, 46, 45, 46, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 109 | [46, 46, 47, 47, 47, 48, 45, 46, 46, 46, 46, 46, 46, 46, 45, 45, 43, 43, 42, 42, 43, 42, 41, 41, 42, 41, 42, 41, 41, 42, 42, 42, 43, 42, 43, 44, 44, 43, 45, 44, 44, 44, 46, 46, 45, 47, 47, 46, 47, 45, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 110 | [47, 47, 48, 48, 48, 48, 47, 48, 47, 45, 46, 45, 46, 44, 44, 43, 43, 42, 42, 41, 42, 40, 41, 41, 40, 40, 41, 40, 40, 42, 41, 40, 42, 42, 42, 42, 42, 42, 43, 43, 45, 45, 44, 45, 48, 47, 45, 45, 45, 46, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 111 | [47, 46, 47, 48, 47, 47, 47, 46, 45, 45, 46, 44, 43, 44, 43, 42, 41, 41, 41, 40, 40, 40, 40, 39, 39, 40, 39, 39, 40, 39, 39, 40, 39, 40, 40, 41, 42, 42, 43, 44, 42, 44, 44, 45, 45, 46, 46, 46, 45, 45, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 112 | [46, 47, 48, 47, 47, 46, 46, 46, 45, 44, 44, 44, 44, 42, 41, 41, 41, 40, 40, 39, 39, 39, 38, 38, 39, 38, 38, 39, 39, 38, 39, 38, 39, 40, 40, 40, 41, 41, 42, 42, 43, 43, 43, 44, 43, 45, 45, 46, 45, 47, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 113 | [47, 49, 46, 48, 46, 48, 45, 44, 44, 43, 44, 43, 42, 41, 40, 40, 39, 39, 39, 38, 39, 38, 38, 37, 39, 38, 37, 37, 38, 38, 38, 38, 37, 39, 39, 39, 40, 39, 42, 41, 43, 42, 43, 43, 44, 44, 46, 44, 45, 46, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 114 | [48, 49, 47, 48, 47, 48, 46, 46, 44, 43, 42, 42, 43, 41, 40, 39, 38, 38, 39, 38, 38, 37, 37, 37, 36, 38, 36, 36, 37, 37, 36, 37, 37, 38, 38, 39, 38, 40, 40, 41, 41, 41, 42, 43, 43, 45, 44, 46, 45, 46, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 115 | [47, 47, 49, 47, 45, 45, 46, 44, 44, 43, 42, 41, 41, 40, 40, 39, 38, 38, 38, 37, 37, 37, 36, 36, 37, 36, 35, 36, 36, 36, 36, 37, 37, 38, 39, 39, 38, 39, 40, 41, 40, 40, 42, 41, 42, 43, 44, 46, 44, 45, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 116 | [47, 46, 48, 47, 47, 45, 44, 43, 45, 43, 41, 41, 39, 40, 38, 37, 38, 37, 37, 37, 36, 36, 36, 36, 35, 35, 36, 34, 36, 35, 35, 37, 36, 37, 37, 37, 38, 38, 38, 40, 40, 41, 43, 42, 42, 43, 44, 44, 44, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 117 | [48, 47, 46, 46, 47, 45, 46, 44, 43, 42, 41, 41, 40, 38, 39, 39, 38, 37, 36, 34, 35, 35, 34, 34, 34, 34, 34, 34, 35, 35, 34, 35, 35, 36, 35, 37, 37, 37, 38, 39, 40, 40, 41, 41, 42, 43, 44, 45, 45, 44, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 118 | [47, 48, 47, 46, 45, 45, 44, 43, 44, 42, 41, 40, 40, 39, 38, 37, 37, 36, 35, 35, 35, 33, 35, 35, 34, 34, 34, 35, 34, 34, 34, 35, 36, 35, 36, 37, 37, 38, 38, 40, 38, 39, 40, 42, 41, 44, 44, 45, 43, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 119 | [48, 47, 46, 48, 44, 44, 43, 42, 42, 40, 39, 40, 39, 38, 37, 37, 36, 35, 36, 35, 35, 34, 34, 34, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, 38, 38, 39, 39, 41, 40, 41, 43, 42, 44, 45, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 120 | [47, 47, 47, 46, 46, 45, 43, 43, 42, 42, 40, 38, 40, 37, 37, 36, 36, 36, 34, 35, 34, 33, 34, 33, 33, 33, 33, 33, 34, 33, 34, 33, 36, 35, 35, 35, 36, 37, 38, 38, 38, 39, 40, 41, 42, 42, 42, 45, 43, 44, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 121 | [47, 48, 48, 47, 46, 44, 44, 42, 42, 40, 40, 40, 39, 38, 37, 36, 35, 34, 35, 34, 34, 34, 33, 32, 33, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 36, 37, 37, 37, 38, 39, 38, 40, 40, 41, 42, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 122 | [48, 48, 47, 45, 45, 44, 44, 42, 43, 40, 39, 39, 39, 37, 36, 37, 35, 34, 33, 33, 34, 33, 32, 32, 32, 32, 33, 33, 32, 33, 33, 33, 33, 34, 34, 35, 36, 36, 37, 37, 39, 39, 40, 39, 41, 41, 43, 44, 43, 43, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 123 | [48, 48, 47, 47, 46, 44, 43, 41, 41, 40, 40, 39, 38, 38, 36, 35, 35, 34, 34, 33, 33, 32, 32, 33, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, 34, 35, 36, 37, 37, 38, 39, 39, 41, 41, 42, 43, 43, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 124 | [46, 47, 46, 45, 44, 44, 42, 42, 41, 41, 39, 38, 39, 36, 37, 35, 35, 35, 33, 34, 33, 33, 32, 33, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 35, 35, 35, 37, 37, 37, 39, 40, 40, 42, 41, 42, 43, 43, 42, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 125 | [49, 47, 48, 46, 44, 46, 45, 43, 42, 40, 39, 39, 37, 37, 36, 35, 35, 35, 34, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 33, 35, 35, 35, 36, 35, 37, 38, 39, 39, 39, 42, 42, 42, 43, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 126 | [49, 48, 47, 46, 46, 44, 43, 43, 41, 40, 39, 38, 38, 36, 36, 36, 34, 34, 34, 34, 33, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 34, 34, 34, 34, 35, 35, 36, 36, 37, 39, 39, 40, 41, 40, 42, 42, 44, 43, 45, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 127 | [48, 47, 46, 48, 44, 45, 43, 43, 41, 42, 39, 39, 38, 38, 37, 35, 35, 34, 35, 33, 33, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, 36, 37, 38, 38, 39, 40, 40, 41, 42, 42, 43, 44, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 128 | [48, 48, 46, 46, 46, 44, 42, 43, 42, 41, 40, 39, 38, 37, 36, 35, 35, 35, 34, 33, 33, 33, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 36, 35, 36, 38, 38, 40, 41, 41, 42, 43, 42, 43, 44, 44, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 129 | [48, 49, 46, 46, 46, 45, 44, 42, 42, 40, 40, 39, 38, 38, 36, 36, 35, 35, 34, 34, 34, 33, 32, 33, 32, 32, 32, 33, 32, 32, 32, 33, 34, 33, 34, 35, 36, 36, 38, 39, 38, 39, 40, 41, 41, 43, 43, 42, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 130 | [47, 47, 46, 47, 45, 44, 43, 43, 42, 41, 41, 39, 38, 38, 36, 37, 35, 36, 34, 34, 34, 34, 32, 32, 32, 33, 32, 32, 32, 33, 34, 34, 34, 35, 35, 35, 35, 37, 37, 37, 39, 38, 41, 41, 40, 42, 43, 43, 44, 44, 44, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48], 131 | [47, 49, 47, 46, 45, 45, 43, 42, 44, 43, 40, 38, 38, 38, 38, 37, 35, 35, 35, 34, 34, 33, 33, 33, 32, 32, 33, 33, 33, 33, 34, 36, 34, 35, 35, 36, 36, 36, 37, 38, 39, 40, 40, 40, 42, 42, 43, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 132 | [47, 46, 46, 47, 46, 44, 44, 43, 42, 40, 42, 40, 39, 39, 38, 37, 37, 36, 35, 36, 35, 35, 33, 34, 34, 33, 33, 33, 32, 34, 33, 34, 35, 35, 36, 37, 37, 36, 39, 38, 40, 41, 41, 41, 42, 42, 44, 43, 44, 44, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 133 | [48, 49, 47, 47, 47, 45, 45, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 36, 35, 36, 35, 34, 34, 34, 34, 33, 34, 34, 34, 35, 35, 36, 35, 35, 36, 37, 38, 37, 39, 40, 40, 41, 41, 42, 43, 43, 44, 44, 43, 44, 44, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 134 | [47, 47, 47, 44, 46, 47, 46, 44, 43, 43, 42, 40, 40, 39, 38, 38, 38, 38, 35, 36, 36, 35, 35, 35, 35, 34, 35, 34, 35, 35, 35, 35, 36, 36, 37, 37, 36, 39, 40, 40, 39, 42, 41, 42, 42, 42, 43, 44, 45, 45, 43, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 135 | [46, 47, 47, 47, 47, 46, 45, 44, 45, 43, 42, 41, 41, 40, 39, 37, 37, 37, 36, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 38, 38, 40, 40, 39, 41, 41, 40, 42, 41, 44, 44, 44, 45, 46, 46, 46, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 136 | [47, 48, 47, 48, 47, 47, 45, 45, 44, 44, 43, 42, 42, 41, 40, 39, 38, 38, 38, 37, 36, 36, 37, 36, 36, 36, 37, 36, 36, 36, 37, 36, 37, 38, 37, 38, 40, 39, 40, 41, 40, 41, 42, 42, 43, 42, 44, 44, 44, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 137 | [46, 46, 48, 47, 47, 46, 47, 44, 45, 44, 44, 42, 42, 41, 41, 39, 39, 39, 38, 39, 39, 38, 38, 37, 37, 38, 37, 37, 37, 38, 38, 37, 38, 39, 39, 39, 39, 40, 41, 41, 41, 41, 42, 43, 44, 44, 44, 45, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 138 | [46, 46, 47, 48, 48, 46, 46, 45, 45, 44, 43, 44, 43, 42, 41, 41, 41, 40, 40, 38, 39, 39, 39, 37, 38, 38, 38, 38, 38, 39, 38, 40, 39, 39, 40, 40, 40, 41, 41, 41, 41, 44, 44, 45, 44, 45, 45, 45, 45, 45, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44], 139 | [47, 48, 47, 48, 48, 48, 47, 46, 46, 45, 43, 44, 43, 42, 42, 41, 41, 40, 40, 40, 40, 40, 39, 40, 38, 40, 38, 40, 39, 39, 39, 39, 40, 39, 41, 41, 41, 40, 41, 42, 42, 43, 44, 44, 43, 45, 46, 45, 46, 45, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 140 | [47, 47, 47, 47, 47, 47, 48, 46, 45, 45, 46, 43, 44, 43, 43, 44, 41, 42, 42, 41, 41, 40, 40, 40, 40, 40, 39, 39, 40, 41, 40, 41, 41, 41, 41, 42, 40, 42, 42, 43, 42, 45, 44, 45, 45, 44, 45, 45, 45, 45, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 141 | [45, 47, 47, 47, 46, 47, 49, 47, 47, 46, 46, 46, 44, 44, 44, 43, 43, 42, 41, 42, 42, 40, 42, 40, 41, 40, 40, 41, 42, 41, 42, 41, 42, 42, 42, 42, 42, 42, 44, 44, 45, 45, 44, 45, 45, 45, 45, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 142 | [45, 46, 46, 48, 48, 48, 47, 47, 47, 48, 47, 45, 46, 46, 44, 44, 44, 42, 43, 42, 42, 42, 42, 40, 42, 43, 41, 43, 42, 42, 42, 41, 42, 44, 43, 43, 43, 43, 44, 45, 45, 45, 45, 45, 45, 45, 45, 46, 45, 45, 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], 143 | [46, 45, 47, 46, 47, 48, 47, 46, 46, 46, 46, 46, 45, 46, 45, 45, 45, 43, 45, 43, 44, 43, 43, 43, 43, 43, 43, 44, 42, 42, 45, 42, 43, 44, 46, 43, 44, 45, 45, 47, 45, 46, 45, 46, 46, 45, 46, 46, 45, 42, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45], 144 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 145 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 146 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 147 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 148 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 149 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 150 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 151 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 152 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 153 | [45, 45, 45, 46, 47, 49, 47, 47, 48, 48, 49, 48, 47, 47, 46, 47, 46, 44, 46, 46, 44, 45, 44, 46, 44, 44, 44, 44, 43, 44, 44, 45, 44, 45, 44, 44, 46, 45, 46, 47, 45, 46, 47, 48, 46, 46, 46, 46, 45, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], 154 | ], 155 | [ 156 | [40, 40, 40, 40, 39, 41, 39, 40, 39, 39, 40, 39, 38, 38, 38, 37, 38, 38, 37, 37, 37, 38, 37, 38, 37, 36, 36, 36, 39, 36, 37, 39, 37, 36, 37, 37, 39, 37, 39, 38, 38, 38, 40, 39, 40, 39, 40, 40, 40, 41, 40, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42], 157 | [41, 40, 39, 40, 40, 40, 41, 40, 38, 38, 39, 39, 40, 39, 38, 38, 38, 37, 37, 37, 37, 36, 36, 36, 35, 35, 36, 37, 36, 36, 36, 36, 37, 37, 38, 37, 38, 37, 37, 38, 39, 37, 38, 39, 38, 39, 38, 40, 40, 40, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 158 | [40, 40, 40, 40, 39, 41, 40, 39, 39, 40, 39, 38, 37, 37, 37, 37, 38, 38, 36, 38, 36, 36, 36, 36, 35, 36, 36, 37, 36, 36, 36, 36, 37, 37, 36, 36, 37, 38, 37, 38, 38, 39, 38, 37, 37, 39, 40, 39, 40, 40, 42, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 159 | [40, 40, 39, 40, 39, 40, 40, 40, 41, 39, 37, 38, 37, 36, 37, 37, 38, 36, 36, 35, 35, 35, 35, 35, 37, 36, 34, 35, 36, 35, 36, 35, 36, 36, 36, 37, 36, 35, 37, 37, 37, 38, 38, 39, 39, 38, 39, 40, 39, 39, 41, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 160 | [41, 39, 39, 40, 39, 40, 38, 39, 39, 38, 38, 38, 38, 37, 37, 36, 36, 36, 35, 35, 35, 35, 35, 36, 36, 34, 34, 36, 35, 35, 35, 36, 35, 35, 35, 35, 36, 37, 37, 36, 37, 37, 38, 37, 37, 38, 38, 38, 39, 40, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 161 | [39, 40, 41, 40, 39, 39, 39, 39, 38, 39, 39, 37, 36, 36, 36, 35, 36, 35, 36, 35, 35, 34, 35, 34, 34, 35, 33, 35, 35, 34, 35, 35, 34, 35, 35, 35, 36, 35, 36, 35, 36, 37, 37, 37, 37, 38, 38, 38, 40, 41, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 162 | [39, 41, 40, 40, 39, 39, 40, 39, 38, 38, 37, 37, 36, 36, 35, 35, 36, 35, 35, 35, 34, 35, 35, 34, 35, 34, 35, 35, 35, 34, 34, 35, 34, 36, 35, 34, 35, 36, 35, 36, 36, 36, 35, 36, 38, 36, 38, 39, 37, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 163 | [40, 39, 39, 40, 39, 39, 37, 37, 37, 37, 37, 37, 36, 35, 37, 36, 35, 34, 34, 34, 34, 33, 33, 33, 34, 34, 34, 34, 34, 34, 33, 34, 34, 35, 34, 35, 35, 35, 36, 35, 35, 36, 37, 37, 36, 37, 38, 37, 37, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 164 | [40, 40, 40, 40, 39, 38, 37, 39, 37, 37, 37, 36, 36, 35, 35, 35, 35, 36, 34, 36, 33, 35, 34, 34, 34, 34, 33, 33, 34, 33, 34, 33, 35, 34, 34, 34, 34, 35, 34, 35, 36, 36, 36, 35, 38, 36, 37, 36, 37, 37, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 165 | [39, 40, 40, 40, 39, 40, 37, 39, 37, 37, 37, 37, 35, 36, 35, 34, 33, 34, 33, 34, 33, 35, 34, 34, 34, 33, 33, 33, 33, 34, 34, 34, 34, 34, 33, 35, 34, 34, 34, 35, 35, 36, 37, 36, 36, 36, 36, 37, 38, 37, 39, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 166 | [40, 39, 40, 39, 39, 38, 38, 37, 36, 36, 37, 36, 35, 35, 35, 35, 33, 34, 34, 33, 34, 33, 33, 33, 33, 34, 33, 33, 34, 33, 34, 33, 33, 34, 33, 34, 34, 35, 34, 35, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 167 | [40, 38, 41, 40, 39, 39, 38, 39, 37, 36, 36, 35, 35, 35, 35, 34, 34, 34, 33, 33, 32, 33, 33, 33, 34, 34, 33, 34, 34, 33, 32, 32, 33, 34, 33, 33, 35, 35, 34, 35, 35, 35, 35, 37, 36, 36, 35, 36, 37, 36, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 168 | [40, 38, 40, 40, 39, 38, 37, 37, 37, 36, 36, 35, 34, 36, 34, 34, 33, 34, 34, 32, 33, 33, 34, 33, 33, 32, 33, 33, 33, 32, 32, 33, 33, 34, 33, 33, 33, 34, 34, 35, 35, 35, 36, 36, 36, 36, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 169 | [39, 39, 39, 39, 40, 40, 38, 38, 36, 35, 37, 35, 35, 35, 35, 33, 34, 33, 33, 33, 33, 33, 32, 34, 32, 33, 33, 33, 33, 32, 33, 32, 32, 33, 33, 33, 34, 33, 33, 34, 34, 35, 35, 36, 35, 35, 36, 36, 38, 38, 39, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 170 | [41, 40, 40, 39, 38, 37, 39, 38, 37, 36, 35, 35, 34, 34, 34, 34, 33, 34, 33, 33, 33, 32, 33, 32, 32, 33, 32, 32, 33, 32, 32, 33, 32, 33, 32, 36, 33, 34, 34, 34, 33, 35, 35, 35, 36, 36, 36, 38, 37, 38, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 171 | [40, 40, 38, 40, 37, 38, 38, 37, 37, 36, 35, 34, 34, 34, 35, 33, 33, 33, 33, 33, 32, 33, 33, 33, 33, 33, 32, 33, 33, 32, 33, 34, 33, 33, 34, 33, 32, 34, 34, 34, 34, 35, 35, 35, 36, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 172 | [40, 40, 40, 40, 39, 39, 38, 37, 36, 35, 36, 35, 34, 34, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 33, 33, 32, 33, 34, 34, 34, 34, 34, 34, 35, 36, 35, 37, 36, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 173 | [41, 38, 39, 39, 38, 39, 38, 37, 36, 35, 35, 34, 34, 34, 34, 32, 33, 33, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 33, 33, 32, 32, 33, 33, 32, 32, 33, 34, 34, 33, 34, 34, 35, 35, 35, 35, 36, 37, 36, 36, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 174 | [40, 39, 40, 40, 38, 38, 36, 37, 37, 37, 35, 35, 34, 33, 34, 33, 32, 32, 32, 32, 33, 32, 32, 33, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 33, 33, 32, 34, 33, 34, 34, 34, 35, 36, 35, 36, 37, 38, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 175 | [41, 39, 40, 38, 38, 37, 37, 37, 36, 35, 36, 35, 34, 33, 33, 33, 32, 33, 33, 32, 32, 33, 33, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 33, 33, 33, 32, 32, 33, 34, 35, 34, 34, 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 176 | [41, 40, 40, 39, 38, 39, 38, 37, 36, 36, 35, 35, 35, 33, 34, 33, 33, 34, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 35, 33, 35, 35, 36, 35, 37, 37, 37, 37, 38, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 177 | [41, 40, 39, 39, 37, 38, 36, 37, 36, 35, 36, 36, 34, 33, 34, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 33, 33, 35, 34, 35, 35, 34, 35, 36, 35, 38, 37, 36, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 178 | [39, 40, 39, 38, 39, 39, 36, 37, 36, 36, 35, 34, 35, 33, 33, 33, 34, 33, 33, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 33, 32, 32, 32, 33, 34, 33, 34, 35, 35, 36, 36, 37, 36, 37, 36, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 179 | [40, 39, 40, 38, 39, 37, 37, 36, 37, 35, 35, 34, 35, 34, 33, 34, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 33, 34, 34, 34, 35, 36, 35, 35, 36, 37, 38, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 180 | [41, 39, 40, 40, 39, 37, 38, 36, 36, 36, 37, 36, 34, 34, 33, 33, 33, 33, 32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 32, 33, 33, 34, 35, 34, 35, 35, 36, 38, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 181 | [40, 38, 39, 40, 39, 37, 37, 37, 35, 36, 35, 35, 34, 33, 33, 33, 32, 32, 33, 32, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 33, 34, 33, 34, 35, 35, 35, 36, 36, 35, 37, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37], 182 | [41, 40, 39, 38, 38, 39, 37, 38, 36, 36, 35, 35, 35, 35, 34, 34, 34, 32, 32, 33, 34, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 33, 33, 32, 33, 34, 34, 35, 34, 35, 35, 35, 35, 36, 37, 36, 37, 38, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 183 | [41, 38, 39, 38, 39, 37, 38, 37, 37, 36, 36, 35, 35, 34, 35, 33, 34, 33, 34, 32, 33, 32, 32, 33, 32, 32, 32, 33, 33, 32, 32, 33, 32, 33, 32, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, 35, 38, 37, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 184 | [40, 39, 39, 39, 39, 39, 37, 37, 37, 37, 37, 35, 35, 35, 33, 35, 35, 33, 33, 34, 32, 32, 33, 32, 32, 32, 33, 32, 33, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 34, 36, 34, 36, 36, 36, 37, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 185 | [41, 41, 39, 39, 39, 38, 37, 37, 37, 37, 36, 36, 35, 35, 34, 34, 34, 33, 34, 33, 32, 33, 34, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 33, 33, 34, 34, 34, 35, 35, 35, 37, 37, 37, 37, 36, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38], 186 | [40, 41, 39, 40, 40, 37, 39, 38, 36, 37, 37, 36, 35, 35, 34, 34, 34, 34, 33, 33, 33, 33, 33, 33, 33, 34, 32, 32, 32, 32, 34, 33, 32, 33, 34, 34, 34, 33, 35, 34, 35, 36, 35, 36, 35, 35, 37, 37, 37, 37, 38, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 187 | [40, 39, 41, 39, 39, 38, 39, 37, 38, 36, 37, 36, 35, 34, 35, 35, 34, 34, 34, 34, 33, 32, 33, 32, 33, 34, 33, 33, 33, 34, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, 37, 35, 37, 37, 39, 38, 38, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 188 | [39, 41, 40, 38, 39, 39, 38, 37, 37, 37, 37, 37, 36, 35, 35, 36, 34, 34, 35, 34, 32, 33, 34, 33, 34, 33, 33, 34, 34, 33, 34, 33, 34, 34, 34, 34, 35, 35, 34, 35, 33, 36, 36, 36, 37, 36, 36, 37, 37, 40, 38, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 189 | [39, 39, 40, 39, 40, 39, 40, 39, 38, 39, 38, 38, 35, 36, 36, 35, 35, 36, 34, 34, 33, 33, 33, 33, 34, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 36, 34, 33, 35, 35, 36, 36, 36, 36, 37, 39, 37, 37, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 190 | [40, 40, 41, 41, 39, 40, 38, 39, 38, 38, 37, 39, 37, 36, 36, 35, 36, 35, 35, 35, 35, 34, 34, 34, 33, 35, 34, 34, 35, 33, 34, 33, 35, 34, 34, 34, 35, 35, 35, 36, 36, 37, 36, 36, 37, 37, 38, 37, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 191 | [40, 39, 38, 41, 40, 39, 39, 37, 38, 39, 39, 37, 37, 36, 38, 35, 35, 35, 35, 35, 34, 34, 35, 35, 34, 35, 35, 33, 34, 35, 35, 33, 34, 35, 36, 35, 36, 35, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 39, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 192 | [39, 39, 41, 41, 39, 39, 39, 40, 37, 38, 38, 37, 38, 37, 36, 35, 38, 36, 35, 35, 36, 35, 35, 35, 34, 35, 35, 37, 35, 34, 35, 36, 35, 35, 36, 35, 35, 36, 35, 35, 35, 36, 36, 37, 36, 38, 37, 39, 37, 37, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40], 193 | [39, 40, 42, 39, 40, 40, 39, 40, 39, 39, 38, 38, 38, 37, 37, 36, 36, 37, 37, 36, 34, 36, 36, 35, 35, 35, 35, 36, 35, 36, 35, 35, 35, 35, 37, 35, 36, 38, 37, 36, 36, 35, 37, 36, 37, 39, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39], 194 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 195 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 196 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 197 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 198 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 199 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 200 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 201 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 202 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 203 | [42, 41, 41, 39, 39, 40, 41, 41, 40, 41, 40, 39, 37, 38, 39, 37, 38, 36, 38, 38, 37, 37, 36, 36, 37, 36, 36, 36, 36, 38, 37, 37, 37, 36, 38, 38, 38, 38, 38, 37, 37, 38, 39, 38, 39, 39, 39, 39, 39, 41, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41], 204 | ] 205 | ] 206 | ) 207 | 208 | np.shape(arducam_cs) 209 | 210 | def get_lens_shading(version): 211 | global arducam_cs 212 | if version == "arducam_cs": return arducam_cs 213 | return None -------------------------------------------------------------------------------- /birbcam/optioncounter.py: -------------------------------------------------------------------------------- 1 | class OptionCounter: 2 | def __init__(self, min, max, increment, start = None): 3 | self.min = min 4 | self.max = max 5 | self.increment = increment 6 | 7 | if start == None: 8 | self.current = min 9 | else: 10 | self.current = start 11 | 12 | @property 13 | def value(self): 14 | return self.current 15 | 16 | @property 17 | def label(self): 18 | return f"{self.current}" 19 | 20 | def next(self): 21 | i = self.current + self.increment 22 | 23 | if i > self.max: 24 | i = self.max 25 | 26 | self.current = i 27 | return i 28 | 29 | def previous(self): 30 | i = self.current - self.increment 31 | 32 | if i < self.min: 33 | i = self.min 34 | 35 | self.current = i 36 | return i -------------------------------------------------------------------------------- /birbcam/optionflipper.py: -------------------------------------------------------------------------------- 1 | class OptionFlipper: 2 | def __init__(self, options, start = 0, labels = None): 3 | self.options = options 4 | self.index = start 5 | self.labels = labels 6 | 7 | @property 8 | def value(self): 9 | return self.options[self.index] 10 | 11 | @property 12 | def label(self): 13 | if self.labels == None: 14 | return f"{self.value}" 15 | else: 16 | return self.labels[self.index] 17 | 18 | @property 19 | def is_at_start(self): 20 | return self.index == 0 21 | 22 | @property 23 | def is_at_end(self): 24 | return self.index == len(self.options) - 1 25 | 26 | def next(self): 27 | return self.__flip(1) 28 | 29 | def previous(self): 30 | return self.__flip(-1) 31 | 32 | def __flip(self, delta): 33 | try: 34 | self.index += delta 35 | 36 | if self.index >= len(self.options): 37 | self.index = 0 38 | 39 | if self.index < 0: 40 | self.index = len(self.options) - 1 41 | except ValueError: 42 | self.index = 0 43 | 44 | return self.options[self.index] -------------------------------------------------------------------------------- /birbcam/picturelogger/__init__.py: -------------------------------------------------------------------------------- 1 | from .picturelogger import PictureLogger -------------------------------------------------------------------------------- /birbcam/picturelogger/picturelogger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from time import time 3 | 4 | class PictureLogger: 5 | def __init__(self, file_source): 6 | self._loggedPictures = [] 7 | #self.__read_picture_history(file_source) 8 | 9 | self._log_file = open(file_source, mode="a", encoding="utf-8") 10 | 11 | def __del__(self): 12 | self._log_file.close() 13 | 14 | def log_picture(self, fullPath, thumbPath, classification, shutter, iso): 15 | entry = { 16 | "full": fullPath, 17 | "thumb": thumbPath, 18 | "evaluation": [dictify_classification(c) for c in classification], 19 | "time": time(), 20 | "shutter": shutter, 21 | "iso": iso 22 | } 23 | 24 | self.__append_to_file(entry) 25 | 26 | def __read_picture_history(self, file_source): 27 | f = open(file_source, mode="r", encoding="utf-8") 28 | for line in f: 29 | self._loggedPictures.append(self.__deserialize_entry(line)) 30 | 31 | def __append_to_file(self, entry): 32 | self._loggedPictures.append(entry) 33 | self._log_file.write(self.__serialize_entry(entry)) 34 | self._log_file.write("\n") 35 | self._log_file.flush() 36 | 37 | def __serialize_entry(self, entry): 38 | c = self.__serialize_classification(entry["evaluation"]) 39 | return f"{entry['time']}|{entry['full']}|{entry['thumb']}|{c}|{entry['shutter']}|{entry['iso']}" 40 | 41 | def __deserialize_entry(self, string): 42 | split = string.split("|") 43 | return { 44 | "time": split[0], 45 | "full": split[1], 46 | "thumb": split[2], 47 | "evaluation": self.__deserialize_classification(split[3]), 48 | "shutter": split[4], 49 | "iso": split[5] 50 | } 51 | 52 | def __serialize_classification(self, results): 53 | strings = [stringify_result(r) for r in results] 54 | return "@".join(strings) 55 | 56 | def __deserialize_classification(self, string): 57 | split = string.split("@") 58 | return [destringify_result(s) for s in split] 59 | 60 | def dictify_classification(classification): 61 | return { 62 | "label": classification.label, 63 | "confidence": classification.confidence 64 | } 65 | 66 | def stringify_result(result): 67 | return f"{result['label']}~{result['confidence']}" 68 | 69 | def destringify_result(result): 70 | split = result.split("~") 71 | return { 72 | "label": split[0], 73 | "confidence": split[1] 74 | } -------------------------------------------------------------------------------- /birbcam/picturetaker.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from datetime import datetime 3 | 4 | class PictureTaker: 5 | def __init__(self, resolution, cooldown, saveTo, fileNamer): 6 | self.nextPictureTime = 0 7 | self.resolution = resolution 8 | self.cooldown = cooldown 9 | self.saveTo = saveTo 10 | self.fileNamer = fileNamer 11 | 12 | @property 13 | def readyForPicture(self): 14 | return time() >= self.nextPictureTime 15 | 16 | def take_picture(self, camera): 17 | if not self.readyForPicture: 18 | return (False, None) 19 | 20 | filename = self.fileNamer() 21 | filepath = self.__save_path(filename) 22 | 23 | restoreResolution = camera.resolution 24 | camera.resolution = self.resolution 25 | camera.capture(filepath) 26 | camera.resolution = restoreResolution 27 | 28 | self.__schedule_next_picture() 29 | return (True, filename, filepath) 30 | 31 | def __save_path(self, name = None): 32 | if name == None: 33 | name = self.fileNamer() 34 | 35 | return f"{self.saveTo}/{name}" 36 | 37 | def reset_time(self): 38 | self.__schedule_next_picture() 39 | 40 | def __schedule_next_picture(self): 41 | self.nextPictureTime = time() + self.cooldown 42 | 43 | def filename_live_picture(): 44 | return "live.jpg" 45 | 46 | def filename_filestamp(): 47 | date = datetime.now() 48 | return date.strftime("%Y-%m-%d-%H-%M-%S") + ".jpg" -------------------------------------------------------------------------------- /birbcam/rectanglegrabber.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | class RectangleGrabber: 4 | def __init__(self, windowName, resolution, onDrag = None, onEnd = None, preserveAspectRatio = False): 5 | self.resolution = resolution 6 | self.start = (0,0) 7 | self.end = resolution 8 | self._isDragging = False 9 | self.onDrag = onDrag 10 | self.onEnd = onEnd 11 | self.preserveAspectRatio = preserveAspectRatio 12 | 13 | if preserveAspectRatio: 14 | self.aspectRatio = resolution[1] / resolution[0] 15 | 16 | cv2.setMouseCallback(windowName, self.__click_handler) 17 | 18 | @property 19 | def isDragging(self): return self._isDragging 20 | 21 | @property 22 | def bounds(self): 23 | if self.start[0] <= self.end[0] and self.start[1] <= self.end[1]: 24 | return (self.start, self.end) 25 | elif self.start[0] > self.end[0] and self.start[1] > self.end[1]: 26 | return (self.end, self.start) 27 | elif self.start[0] > self.end[0]: 28 | return ((self.end[0], self.start[1]), (self.start[0], self.end[1])) 29 | else: 30 | return ((self.start[0], self.end[1]), (self.end[0], self.start[1])) 31 | 32 | def reset(self): 33 | self.start = (0,0) 34 | self.end = self.resolution 35 | self._isDragging = False 36 | self.__report_end() 37 | 38 | def __click_handler(self, event, x, y, flags, param): 39 | if event == cv2.EVENT_LBUTTONDOWN: 40 | self.start = (x, y) 41 | self.end = (x, y) 42 | self._isDragging = True 43 | return 44 | 45 | if event == cv2.EVENT_LBUTTONUP: 46 | self._isDragging = False 47 | self.__report_end() 48 | return 49 | 50 | if event == cv2.EVENT_MOUSEMOVE: 51 | if self.isDragging: 52 | self.end = self.__calculate_br(x, y) 53 | 54 | if self.onDrag != None: 55 | self.onDrag(self.bounds) 56 | return 57 | 58 | def __calculate_br(self, brx, bry): 59 | if not self.preserveAspectRatio: 60 | return (brx, bry) 61 | 62 | dx = brx - self.start[0] 63 | dy = bry - self.start[1] 64 | h = round(dx * self.aspectRatio) 65 | 66 | if dx > 0 and dy < 0 or dx < 0 and dy > 0: 67 | h *= -1 68 | 69 | return (brx, self.start[1] + h) 70 | 71 | def __report_end(self): 72 | if self.onEnd != None: 73 | self.onEnd(self.bounds) 74 | 75 | -------------------------------------------------------------------------------- /birbcam/viewfinder.py: -------------------------------------------------------------------------------- 1 | from picamera.array import PiRGBArray 2 | from picamera import PiCamera 3 | from birbloop import BirbWatcher, setup_logging 4 | from common import draw_mask 5 | import time 6 | import cv2 7 | import numpy as np 8 | 9 | previewResolution = (640, 480) 10 | mask = (0.5, 0.5) 11 | windowName = 'preview' 12 | 13 | def get_next_exposure(exp): 14 | return get_next_enum(PiCamera.EXPOSURE_MODES, exp) 15 | 16 | def get_next_meter(meter): 17 | return get_next_enum(PiCamera.METER_MODES, meter) 18 | 19 | def get_next_enum(enum, exp): 20 | modes = list(enum) 21 | useNext = False 22 | 23 | for mode in modes: 24 | if useNext: 25 | return mode 26 | 27 | if mode == exp: 28 | useNext = True 29 | 30 | return modes[0] 31 | 32 | def get_meter_rect(meter): 33 | width = previewResolution[0] 34 | height = previewResolution[1] 35 | halfW = width / 2 36 | halfH = height / 2 37 | 38 | modes = { 39 | "average": (int(halfW - halfW * 0.25), int(halfH - halfH * 0.25), int(width * 0.25), int(height * 0.25)), 40 | "spot": (int(halfW - halfW * 0.1), int(halfH - halfH * 0.1), int(width * 0.1), int(height * 0.1)), 41 | "backlit": (int(halfW - halfW * 0.3), int(halfH - halfH * 0.3), int(width * 0.3), int(height * 0.3)), 42 | "matrix": (0, 0, width, height) 43 | } 44 | 45 | return modes[meter] 46 | 47 | def mask_expand_horizontal(size): 48 | x = size[0] 49 | if x < 1: 50 | x += 0.05 51 | return (x, size[1]) 52 | 53 | def mask_contract_horizontal(size): 54 | x = size[0] 55 | if x > 0: 56 | x -= 0.05 57 | return (x, size[1]) 58 | 59 | def mask_expand_vertical(size): 60 | y = size[1] 61 | if y < 1: 62 | y += 0.05 63 | return (size[0], y) 64 | 65 | def mask_contract_vertical(size): 66 | y = size[1] 67 | if y > 0: 68 | y -= 0.05 69 | return (size[0], y) 70 | 71 | def click_size_mask(event, x, y, flags, param): 72 | if event != cv2.EVENT_LBUTTONDOWN: 73 | return 74 | 75 | print("left mouse down: ", x, ", ", y) 76 | 77 | width = previewResolution[0] 78 | height = previewResolution[1] 79 | cx = int(width / 2) 80 | cy = int(height / 2) 81 | print(" ", cx, ", ", cy) 82 | dx = cx - x 83 | dy = cy - y 84 | print(" ", dx, ", ", dy) 85 | mx = abs(dx / width) * 2 86 | my = abs(dy / height) * 2 87 | print(" ", mx, ", ", my) 88 | 89 | global mask 90 | mask = (mx, my) 91 | 92 | cv2.namedWindow(windowName) 93 | cv2.setMouseCallback(windowName, click_size_mask) 94 | 95 | camera = PiCamera() 96 | camera.resolution = previewResolution 97 | camera.framerate = 32; 98 | rawCapture = PiRGBArray(camera, size=previewResolution) 99 | 100 | time.sleep(0.1) 101 | 102 | for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): 103 | image = frame.array 104 | 105 | draw_mask(image, mask, previewResolution) 106 | 107 | cv2.putText(image,"(E)xposure Mode: " + camera.exposure_mode,(20, 20),cv2.FONT_HERSHEY_SIMPLEX,0.5,(255, 255, 255),1) 108 | cv2.putText(image,"Shutter: %d" % camera.exposure_speed,(20, 40),cv2.FONT_HERSHEY_SIMPLEX,0.5,(255, 255, 255),1) 109 | cv2.putText(image,"(I)SO: %d" % camera.iso,(20, 60),cv2.FONT_HERSHEY_SIMPLEX,0.5,(255, 255, 255),1) 110 | cv2.putText(image,"Meter: " + camera.meter_mode,(20, 80),cv2.FONT_HERSHEY_SIMPLEX,0.5,(255, 255, 255),1) 111 | 112 | (x, y, w, h) = get_meter_rect(camera.meter_mode) 113 | cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 255), 2) 114 | 115 | cv2.imshow(windowName, image) 116 | key = cv2.waitKey(1) & 0xFF 117 | 118 | rawCapture.truncate(0) 119 | 120 | if key == ord("i"): 121 | if camera.iso == 800: 122 | camera.iso = 0 123 | else: 124 | camera.iso = camera.iso + 100 125 | 126 | if key == ord("e"): 127 | camera.exposure_mode = get_next_exposure(camera.exposure_mode) 128 | 129 | #if key == ord("m"): 130 | # camera.meter_mode = get_next_meter(camera.meter_mode) 131 | 132 | if key == ord("v"): 133 | mask = mask_contract_horizontal(mask) 134 | 135 | if key == ord("b"): 136 | mask = mask_expand_horizontal(mask) 137 | 138 | if key == ord("n"): 139 | mask = mask_contract_vertical(mask) 140 | 141 | if key == ord("m"): 142 | mask = mask_expand_vertical(mask) 143 | 144 | if key == ord("q"): 145 | break 146 | 147 | # Closes all the frames 148 | cv2.destroyAllWindows() 149 | camera.close() 150 | 151 | watcher = BirbWatcher(mask) 152 | watcher.run() -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | Directory = /home/pi/Public/birbs 3 | LogFile = ../debug/log.txt 4 | NoCapture = False 5 | Threshold = 80 6 | ContourArea = 500 7 | 8 | [Saving] 9 | Directory = /media/pi/birbstorage 10 | 11 | # Number of seconds between each live picture. Set to 0 to disable 12 | LivePictureInterval = 10 13 | 14 | # Seconds after full picture is taken before another one can be 15 | FullPictureInterval = 10 16 | 17 | # Resolution is a string as dimensions: x 18 | # Note that the camera module only natively supports certain resolutions. Any other resolution will be scaled by the GPU. 19 | # A list of native resolutions can be found on the camera module documentation under the '--mode' flag: https://www.raspberrypi.org/documentation/raspbian/applications/camera.md 20 | FullPictureResolution = 4056x3040 21 | LivePictureResolution = 800x600 22 | 23 | [Detection] 24 | #Threshold = 80 25 | #ContourArea = 500 26 | 27 | # Number of seconds to wait between automatic exposure checks. 0 to turn off. 28 | ExposureInterval = 120 29 | 30 | # Target exposure level for the auto exposure 31 | ExposureLevel = 100 32 | 33 | # Error margin for matching exposure level to the target level 34 | ExposureError = 10 35 | 36 | [Debug] 37 | Enable = True 38 | #NoCapture = False 39 | #LogFile = ./debug/log.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | asn1crypto==0.24.0 3 | astroid==2.1.0 4 | asttokens==1.1.13 5 | automationhat==0.2.0 6 | beautifulsoup4==4.7.1 7 | blinker==1.4 8 | blinkt==0.1.2 9 | buttonshim==0.0.2 10 | Cap1xxx==0.1.3 11 | certifi==2018.8.24 12 | chardet==3.0.4 13 | Click==7.0 14 | colorama==0.3.7 15 | colorzero==1.1 16 | cookies==2.2.1 17 | cryptography==2.6.1 18 | cupshelpers==1.0 19 | cycler==0.10.0 20 | decorator==4.3.0 21 | distlib==0.3.1 22 | docutils==0.14 23 | drumhat==0.1.0 24 | entrypoints==0.3 25 | envirophat==1.0.0 26 | ExplorerHAT==0.4.2 27 | filelock==3.0.12 28 | Flask==1.0.2 29 | fourletterphat==0.1.0 30 | gpiozero==1.5.1 31 | guizero==0.6.0 32 | html5lib==1.0.1 33 | idna==2.6 34 | importlib-metadata==3.4.0 35 | ipykernel==4.9.0 36 | ipython==5.8.0 37 | ipython-genutils==0.2.0 38 | isort==4.3.4 39 | itsdangerous==0.24 40 | jedi==0.13.2 41 | Jinja2==2.10 42 | jupyter-client==5.2.3 43 | jupyter-core==4.4.0 44 | keyring==17.1.1 45 | keyrings.alt==3.1.1 46 | kiwisolver==1.0.1 47 | lazy-object-proxy==1.3.1 48 | logilab-common==1.4.2 49 | lxml==4.3.2 50 | MarkupSafe==1.1.0 51 | matplotlib==3.0.2 52 | mccabe==0.6.1 53 | microdotphat==0.2.1 54 | mote==0.0.4 55 | motephat==0.0.3 56 | mypy==0.670 57 | mypy-extensions==0.4.1 58 | nudatus==0.0.4 59 | numpy==1.16.2 60 | oauthlib==2.1.0 61 | olefile==0.46 62 | pantilthat==0.0.7 63 | parso==0.3.1 64 | pbr==5.5.1 65 | pexpect==4.6.0 66 | pgzero==1.2 67 | phatbeat==0.1.1 68 | pianohat==0.1.0 69 | picamera==1.13 70 | pickleshare==0.7.5 71 | picraft==1.0 72 | piglow==1.2.5 73 | pigpio==1.44 74 | Pillow==5.4.1 75 | prompt-toolkit==1.0.15 76 | psutil==5.5.1 77 | pycairo==1.16.2 78 | pycodestyle==2.4.0 79 | pycrypto==2.6.1 80 | pycups==1.9.73 81 | pyflakes==2.0.0 82 | pygame==1.9.4.post1 83 | Pygments==2.3.1 84 | PyGObject==3.30.4 85 | pyinotify==0.9.6 86 | PyJWT==1.7.0 87 | pylint==2.2.2 88 | pyOpenSSL==19.0.0 89 | pyparsing==2.2.0 90 | pyserial==3.4 91 | pysmbc==1.0.15.6 92 | python-apt==1.8.4.3 93 | python-dateutil==2.7.3 94 | pyxdg==0.25 95 | pyzmq==17.1.2 96 | qtconsole==4.3.1 97 | rainbowhat==0.1.0 98 | reportlab==3.5.13 99 | requests==2.21.0 100 | requests-oauthlib==1.0.0 101 | responses==0.9.0 102 | roman==2.0.0 103 | RPi.GPIO==0.7.0 104 | RTIMULib==7.2.1 105 | scrollphat==0.0.7 106 | scrollphathd==1.2.1 107 | SecretStorage==2.3.1 108 | semver==2.0.1 109 | Send2Trash==1.5.0 110 | sense-emu==1.1 111 | sense-hat==2.2.0 112 | simplegeneric==0.8.1 113 | simplejson==3.16.0 114 | six==1.12.0 115 | skywriter==0.0.7 116 | sn3218==1.2.7 117 | soupsieve==1.8 118 | spidev==3.4 119 | ssh-import-id==5.7 120 | stevedore==3.3.0 121 | thonny==3.3.0 122 | tornado==5.1.1 123 | touchphat==0.0.1 124 | traitlets==4.3.2 125 | twython==3.7.0 126 | typed-ast==1.3.1 127 | typing-extensions==3.7.4.3 128 | uflash==1.2.4 129 | unicornhathd==0.0.4 130 | urllib3==1.24.1 131 | virtualenv==20.3.0 132 | virtualenv-clone==0.5.4 133 | virtualenvwrapper==4.8.4 134 | wcwidth==0.1.7 135 | webencodings==0.5.1 136 | Werkzeug==0.14.1 137 | wrapt==1.10.11 138 | zipp==3.4.0 139 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdpdev/birbcam/b44c95744d81d063f12dfb2521019ff89787c45a/tests/__init__.py -------------------------------------------------------------------------------- /tests/adjust_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from unittest.mock import patch, MagicMock, Mock 5 | from birbcam.exposureadjust.adjust import Adjust 6 | 7 | class TestExposureAdjust(): 8 | @property 9 | def sleepInterval(self): 10 | return 100 11 | 12 | @patch('time.time', MagicMock(return_value=-25)) 13 | def test_not_time_to_update(): 14 | with patch('birbcam.exposureadjust.utils.calculate_exposure', MagicMock(return_value=100)) as mock_calculate_exposure: 15 | mockChangeState = Mock() 16 | adjust = Adjust() 17 | adjust.take_over(None, None, None, mockChangeState, 100, 10) 18 | adjust.update(None, None) 19 | 20 | assert not mock_calculate_exposure.called 21 | 22 | @patch('time.time', MagicMock(return_value=0)) 23 | def test_finish_exposure_adjustment(): 24 | with patch('birbcam.exposureadjust.adjust.calculate_exposure', MagicMock(return_value=100)) as mock_calculate_exposure: 25 | mockChangeState = Mock() 26 | 27 | adjust = Adjust() 28 | adjust.check_exposure = MagicMock(return_value=True) 29 | adjust.finish = Mock() 30 | 31 | adjust.take_over(TestExposureAdjust(), None, None, mockChangeState, 100, 10) 32 | adjust.update(None, None) 33 | 34 | assert adjust.finish.called 35 | 36 | 37 | @patch('time.time', MagicMock(return_value=0)) 38 | def test_do_adjust(): 39 | with patch('birbcam.exposureadjust.adjust.calculate_exposure', MagicMock(return_value=100)) as mock_calculate_exposure: 40 | mockChangeState = Mock() 41 | 42 | adjust = Adjust() 43 | adjust.check_exposure = MagicMock(return_value=False) 44 | adjust.do_adjust = Mock() 45 | 46 | adjust.take_over(TestExposureAdjust(), None, None, mockChangeState, 100, 10) 47 | adjust.update(None, None) 48 | 49 | assert adjust.do_adjust.called -------------------------------------------------------------------------------- /tests/adjustdown_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from unittest.mock import patch, MagicMock, Mock 5 | from birbcam.exposureadjust.adjustdown import AdjustDown 6 | 7 | class TestFlipper(): 8 | __test__ = False 9 | 10 | def __init__(self, isAtStart, step): 11 | self._isAtStart = isAtStart 12 | self._step = step 13 | 14 | @property 15 | def is_at_start(self): 16 | return self._isAtStart 17 | 18 | def previous(self): 19 | return self._step 20 | 21 | class TestCamera(): 22 | __test__ = False 23 | 24 | def __init__(self, expectShutter): 25 | self._expectShutter = expectShutter 26 | 27 | @property 28 | def shutter_speed(self): 29 | return 10000 30 | 31 | @shutter_speed.setter 32 | def shutter_speed(self, value): 33 | assert self._expectShutter == value 34 | 35 | 36 | def test_do_adjust_changes_shutter(): 37 | mockChangeState = Mock() 38 | 39 | adjustdown = AdjustDown() 40 | adjustdown.take_over( 41 | None, 42 | TestFlipper(False, 10000), 43 | TestFlipper(False, 100), 44 | mockChangeState, 45 | 100, 46 | 10 47 | ) 48 | 49 | adjustdown.do_adjust(TestCamera(10000)) 50 | 51 | def test_do_adjust_finish(): 52 | mockChangeState = Mock() 53 | 54 | adjustdown = AdjustDown() 55 | adjustdown.finish = Mock() 56 | adjustdown.take_over( 57 | None, 58 | TestFlipper(True, 10000), 59 | TestFlipper(False, 100), 60 | mockChangeState, 61 | 100, 62 | 10 63 | ) 64 | 65 | adjustdown.do_adjust(TestCamera(10000)) 66 | assert adjustdown.finish.called 67 | 68 | def test_check_exposure_not_finished(): 69 | mockChangeState = Mock() 70 | 71 | adjustdown = AdjustDown() 72 | adjustdown.finish = Mock() 73 | adjustdown.take_over( 74 | None, 75 | TestFlipper(True, 10000), 76 | TestFlipper(False, 100), 77 | mockChangeState, 78 | 100, 79 | 10 80 | ) 81 | 82 | assert not adjustdown.check_exposure(50) 83 | 84 | def test_check_exposure_within_margin(): 85 | mockChangeState = Mock() 86 | 87 | adjustdown = AdjustDown() 88 | adjustdown.finish = Mock() 89 | adjustdown.take_over( 90 | None, 91 | TestFlipper(True, 10000), 92 | TestFlipper(False, 100), 93 | mockChangeState, 94 | 100, 95 | 10 96 | ) 97 | 98 | assert adjustdown.check_exposure(109) 99 | adjustdown.reset_last_exposure() 100 | assert adjustdown.check_exposure(91) 101 | 102 | def test_check_exposure_not_crossed_target(): 103 | mockChangeState = Mock() 104 | 105 | adjustdown = AdjustDown() 106 | adjustdown.finish = Mock() 107 | adjustdown.take_over( 108 | None, 109 | TestFlipper(True, 10000), 110 | TestFlipper(False, 100), 111 | mockChangeState, 112 | 100, 113 | 10 114 | ) 115 | 116 | adjustdown.check_exposure(50) 117 | assert not adjustdown.check_exposure(75) 118 | 119 | @patch('time.time', side_effect=[0, 30]) 120 | @patch('birbcam.exposureadjust.adjust.calculate_exposure', side_effect=[50, 125]) 121 | def test_check_exposure_crossed_target(mock_time, mock_calculate_exposure): 122 | mockChangeState = Mock() 123 | 124 | adjustdown = AdjustDown() 125 | adjustdown.finish = Mock() 126 | adjustdown.take_over( 127 | None, 128 | TestFlipper(False, 10000), 129 | TestFlipper(False, 100), 130 | mockChangeState, 131 | 100, 132 | 10 133 | ) 134 | 135 | assert not adjustdown.check_exposure(50) 136 | adjustdown._lastExposure = 50 137 | assert adjustdown.check_exposure(125) -------------------------------------------------------------------------------- /tests/adjustup_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from unittest.mock import patch, MagicMock, Mock 5 | from birbcam.exposureadjust.adjustup import AdjustUp 6 | 7 | class TestFlipper(): 8 | __test__ = False 9 | 10 | def __init__(self, isAtEnd, step): 11 | self._isAtEnd = isAtEnd 12 | self._step = step 13 | 14 | @property 15 | def is_at_end(self): 16 | return self._isAtEnd 17 | 18 | def next(self): 19 | return self._step 20 | 21 | class TestCamera(): 22 | __test__ = False 23 | 24 | def __init__(self, expectShutter): 25 | self._expectShutter = expectShutter 26 | 27 | @property 28 | def shutter_speed(self): 29 | return 10000 30 | 31 | @shutter_speed.setter 32 | def shutter_speed(self, value): 33 | assert self._expectShutter == value 34 | 35 | 36 | def test_do_adjust_changes_shutter(): 37 | mockChangeState = Mock() 38 | 39 | adjustup = AdjustUp() 40 | adjustup.take_over( 41 | None, 42 | TestFlipper(False, 10000), 43 | TestFlipper(False, 100), 44 | mockChangeState, 45 | 100, 46 | 10 47 | ) 48 | 49 | adjustup.do_adjust(TestCamera(10000)) 50 | 51 | def test_do_adjust_finish(): 52 | mockChangeState = Mock() 53 | 54 | adjustup = AdjustUp() 55 | adjustup.finish = Mock() 56 | adjustup.take_over( 57 | None, 58 | TestFlipper(True, 10000), 59 | TestFlipper(False, 100), 60 | mockChangeState, 61 | 100, 62 | 10 63 | ) 64 | 65 | adjustup.do_adjust(TestCamera(10000)) 66 | assert adjustup.finish.called 67 | 68 | def test_check_exposure_not_finished(): 69 | mockChangeState = Mock() 70 | 71 | adjustup = AdjustUp() 72 | adjustup.finish = Mock() 73 | adjustup.take_over( 74 | None, 75 | TestFlipper(True, 10000), 76 | TestFlipper(False, 100), 77 | mockChangeState, 78 | 100, 79 | 10 80 | ) 81 | 82 | assert not adjustup.check_exposure(50) 83 | 84 | def test_check_exposure_within_margin(): 85 | mockChangeState = Mock() 86 | 87 | adjustup = AdjustUp() 88 | adjustup.finish = Mock() 89 | adjustup.take_over( 90 | None, 91 | TestFlipper(True, 10000), 92 | TestFlipper(False, 100), 93 | mockChangeState, 94 | 100, 95 | 10 96 | ) 97 | 98 | assert adjustup.check_exposure(109) 99 | adjustup.reset_last_exposure() 100 | assert adjustup.check_exposure(91) 101 | 102 | def test_check_exposure_not_crossed_target(): 103 | mockChangeState = Mock() 104 | 105 | adjustup = AdjustUp() 106 | adjustup.finish = Mock() 107 | adjustup.take_over( 108 | None, 109 | TestFlipper(True, 10000), 110 | TestFlipper(False, 100), 111 | mockChangeState, 112 | 100, 113 | 10 114 | ) 115 | 116 | adjustup.check_exposure(50) 117 | assert not adjustup.check_exposure(75) 118 | 119 | @patch('time.time', side_effect=[0, 30]) 120 | @patch('birbcam.exposureadjust.adjust.calculate_exposure', side_effect=[50, 125]) 121 | def test_check_exposure_crossed_target(mock_time, mock_calculate_exposure): 122 | mockChangeState = Mock() 123 | 124 | adjustup = AdjustUp() 125 | adjustup.finish = Mock() 126 | adjustup.take_over( 127 | None, 128 | TestFlipper(False, 10000), 129 | TestFlipper(False, 100), 130 | mockChangeState, 131 | 100, 132 | 10 133 | ) 134 | 135 | assert not adjustup.check_exposure(50) 136 | adjustup._lastExposure = 50 137 | assert adjustup.check_exposure(125) -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 4 | 5 | import birbcam -------------------------------------------------------------------------------- /tests/optioncounter_test.py: -------------------------------------------------------------------------------- 1 | from birbcam.optioncounter import OptionCounter 2 | 3 | def test_should_init(): 4 | counter = OptionCounter(1, 10, 1, 5) 5 | assert counter.value == 5 6 | 7 | def test_should_init_without_start(): 8 | counter = OptionCounter(1, 10, 1) 9 | assert counter.value == 1 10 | 11 | def test_should_increment(): 12 | counter = OptionCounter(1, 10, 1, 5) 13 | counter.next() 14 | assert counter.value == 6 15 | 16 | def test_should_decrement(): 17 | counter = OptionCounter(1, 10, 1, 5) 18 | counter.previous() 19 | assert counter.value == 4 20 | 21 | def test_should_not_increment_beyond_max(): 22 | counter = OptionCounter(1, 10, 2, 9) 23 | counter.next() 24 | assert counter.value == 10 25 | 26 | def test_should_not_decrement_beyond_min(): 27 | counter = OptionCounter(1, 10, 2, 2) 28 | counter.previous() 29 | assert counter.value == 1 -------------------------------------------------------------------------------- /tests/optionflipper_test.py: -------------------------------------------------------------------------------- 1 | from birbcam.optionflipper import OptionFlipper 2 | 3 | def test_should_init(): 4 | flipper = OptionFlipper([1, 2, 3], 1, ["a", "b", "c"]) 5 | assert flipper.value == 2 6 | assert flipper.label == "b" 7 | 8 | def test_should_init_without_labels(): 9 | flipper = OptionFlipper([1, 2, 3], 1) 10 | assert flipper.value == 2 11 | assert flipper.label == "2" 12 | 13 | def test_should_flip_to_next(): 14 | flipper = OptionFlipper(["a", "b", "c"]) 15 | flipper.next() 16 | assert flipper.value == "b" 17 | 18 | def test_should_flip_to_previous(): 19 | flipper = OptionFlipper(["a", "b", "c"], 1) 20 | flipper.previous() 21 | assert flipper.value == "a" 22 | 23 | def test_should_flip_back_to_start(): 24 | flipper = OptionFlipper(["a", "b", "c"], 2) 25 | flipper.next() 26 | assert flipper.value == "a" 27 | 28 | def test_should_flip_forward_to_end(): 29 | flipper = OptionFlipper(["a", "b", "c"], 0) 30 | flipper.previous() 31 | assert flipper.value == "c" -------------------------------------------------------------------------------- /tests/picturetaker_test.py: -------------------------------------------------------------------------------- 1 | from birbcam.picturetaker import PictureTaker, filename_filestamp, filename_live_picture 2 | from time import time 3 | from datetime import datetime 4 | from collections import deque 5 | 6 | class MockCamera: 7 | def __init__(self, startResolution, expectResolution, expectSaveTo): 8 | self.startResolution = startResolution 9 | self.expectResolution = deque(expectResolution) 10 | self.expectSaveTo = expectSaveTo 11 | self._resolution = startResolution 12 | 13 | @property 14 | def resolution(self): 15 | return self._resolution 16 | 17 | @resolution.setter 18 | def resolution(self, r): 19 | expect = self.expectResolution.popleft() 20 | assert expect == r 21 | 22 | def capture(self, path): 23 | assert path == self.expectSaveTo 24 | 25 | def test_filename_live_picture(): 26 | assert filename_live_picture() == "live.jpg" 27 | 28 | def test_filename_filestamp(): 29 | date = datetime.now() 30 | expect = date.strftime("%Y-%m-%d-%H-%M-%S") + ".jpg" 31 | assert expect == filename_filestamp() 32 | 33 | def test_should_immediately_be_ready_to_take_picture(): 34 | taker = PictureTaker("800x600", 5, ".", filename_live_picture) 35 | assert taker.readyForPicture == True 36 | 37 | def test_should_take_picture(): 38 | taker = PictureTaker("1600x1200", 5, ".", filename_live_picture) 39 | mockCamera = MockCamera("800x600", ["1600x1200", "800x600"], "./" + filename_live_picture()) 40 | 41 | assert taker.take_picture(mockCamera) 42 | -------------------------------------------------------------------------------- /tests/rectanglegrabber_test.py: -------------------------------------------------------------------------------- 1 | from birbcam.rectanglegrabber import RectangleGrabber 2 | from cv2 import EVENT_LBUTTONDOWN, EVENT_LBUTTONUP, EVENT_MOUSEMOVE 3 | 4 | def test_tl_to_br_drag(mocker): 5 | mock_handler = None 6 | def mockSetClickHandler(window, handler): 7 | nonlocal mock_handler 8 | mock_handler = handler 9 | 10 | def mock_drag_done(bounds): 11 | assert bounds == ((300, 300), (400, 400)) 12 | 13 | mocker.patch( 14 | 'cv2.setMouseCallback', 15 | mockSetClickHandler 16 | ) 17 | 18 | rect = RectangleGrabber("test", (800,600), onEnd=mock_drag_done) 19 | assert not rect.isDragging 20 | 21 | mock_handler(EVENT_LBUTTONDOWN, 300, 300, "", "") 22 | assert rect.isDragging 23 | assert rect.bounds == ((300, 300), (300, 300)) 24 | 25 | mock_handler(EVENT_MOUSEMOVE, 400, 400, "", "") 26 | assert rect.bounds == ((300, 300), (400, 400)) 27 | 28 | mock_handler(EVENT_LBUTTONUP, 500, 500, "", "") 29 | 30 | def test_tl_to_br_drag_with_aspect_ratio(mocker): 31 | mock_handler = None 32 | def mockSetClickHandler(window, handler): 33 | nonlocal mock_handler 34 | mock_handler = handler 35 | 36 | def mock_drag_done(bounds): 37 | assert bounds == ((0, 0), (800, 800)) 38 | 39 | mocker.patch( 40 | 'cv2.setMouseCallback', 41 | mockSetClickHandler 42 | ) 43 | 44 | rect = RectangleGrabber("test", (800,800), preserveAspectRatio=True, onEnd=mock_drag_done) 45 | assert not rect.isDragging 46 | 47 | mock_handler(EVENT_LBUTTONDOWN, 0, 0, "", "") 48 | assert rect.isDragging 49 | assert rect.bounds == ((0, 0), (0, 0)) 50 | 51 | mock_handler(EVENT_MOUSEMOVE, 800, 600, "", "") 52 | assert rect.bounds == ((0, 0), (800, 800)) 53 | 54 | mock_handler(EVENT_LBUTTONUP, 500, 500, "", "") 55 | 56 | def test_tr_to_bl_drag(mocker): 57 | mock_handler = None 58 | def mockSetClickHandler(window, handler): 59 | nonlocal mock_handler 60 | mock_handler = handler 61 | 62 | def mock_drag_done(bounds): 63 | assert bounds == ((400, 200), (600, 400)) 64 | 65 | mocker.patch( 66 | 'cv2.setMouseCallback', 67 | mockSetClickHandler 68 | ) 69 | 70 | rect = RectangleGrabber("test", (800,600), onEnd=mock_drag_done) 71 | assert not rect.isDragging 72 | 73 | mock_handler(EVENT_LBUTTONDOWN, 600, 200, "", "") 74 | assert rect.isDragging 75 | assert rect.bounds == ((600, 200), (600, 200)) 76 | 77 | mock_handler(EVENT_MOUSEMOVE, 400, 400, "", "") 78 | assert rect.bounds == ((400, 200), (600, 400)) 79 | 80 | mock_handler(EVENT_LBUTTONUP, 400, 400, "", "") 81 | 82 | def test_tr_to_bl_drag_with_aspect_ratio(mocker): 83 | mock_handler = None 84 | def mockSetClickHandler(window, handler): 85 | nonlocal mock_handler 86 | mock_handler = handler 87 | 88 | def mock_drag_done(bounds): 89 | assert bounds == ((400, 0), (800, 400)) 90 | 91 | mocker.patch( 92 | 'cv2.setMouseCallback', 93 | mockSetClickHandler 94 | ) 95 | 96 | rect = RectangleGrabber("test", (800,800), preserveAspectRatio=True, onEnd=mock_drag_done) 97 | assert not rect.isDragging 98 | 99 | mock_handler(EVENT_LBUTTONDOWN, 800, 0, "", "") 100 | assert rect.isDragging 101 | assert rect.bounds == ((800, 0), (800, 0)) 102 | 103 | mock_handler(EVENT_MOUSEMOVE, 400, 200, "", "") 104 | assert rect.bounds == ((400, 0), (800, 400)) 105 | 106 | mock_handler(EVENT_LBUTTONUP, 400, 400, "", "") 107 | 108 | def test_br_to_tl_drag(mocker): 109 | mock_handler = None 110 | def mockSetClickHandler(window, handler): 111 | nonlocal mock_handler 112 | mock_handler = handler 113 | 114 | def mock_drag_done(bounds): 115 | assert bounds == ((400, 400), (600, 600)) 116 | 117 | mocker.patch( 118 | 'cv2.setMouseCallback', 119 | mockSetClickHandler 120 | ) 121 | 122 | rect = RectangleGrabber("test", (800,600), onEnd=mock_drag_done) 123 | assert not rect.isDragging 124 | 125 | mock_handler(EVENT_LBUTTONDOWN, 600, 600, "", "") 126 | assert rect.isDragging 127 | assert rect.bounds == ((600, 600), (600, 600)) 128 | 129 | mock_handler(EVENT_MOUSEMOVE, 400, 400, "", "") 130 | assert rect.bounds == ((400, 400), (600, 600)) 131 | 132 | mock_handler(EVENT_LBUTTONUP, 400, 400, "", "") 133 | 134 | def test_br_to_tl_drag_with_aspect_ratio(mocker): 135 | mock_handler = None 136 | def mockSetClickHandler(window, handler): 137 | nonlocal mock_handler 138 | mock_handler = handler 139 | 140 | def mock_drag_done(bounds): 141 | assert bounds == ((400, 400), (800, 800)) 142 | 143 | mocker.patch( 144 | 'cv2.setMouseCallback', 145 | mockSetClickHandler 146 | ) 147 | 148 | rect = RectangleGrabber("test", (800,800), preserveAspectRatio=True, onEnd=mock_drag_done) 149 | assert not rect.isDragging 150 | 151 | mock_handler(EVENT_LBUTTONDOWN, 800, 800, "", "") 152 | assert rect.isDragging 153 | assert rect.bounds == ((800, 800), (800, 800)) 154 | 155 | mock_handler(EVENT_MOUSEMOVE, 400, 200, "", "") 156 | assert rect.bounds == ((400, 400), (800, 800)) 157 | 158 | mock_handler(EVENT_LBUTTONUP, 400, 400, "", "") 159 | 160 | def test_bl_to_tr_drag(mocker): 161 | mock_handler = None 162 | def mockSetClickHandler(window, handler): 163 | nonlocal mock_handler 164 | mock_handler = handler 165 | 166 | def mock_drag_done(bounds): 167 | assert bounds == ((200, 400), (400, 600)) 168 | 169 | mocker.patch( 170 | 'cv2.setMouseCallback', 171 | mockSetClickHandler 172 | ) 173 | 174 | rect = RectangleGrabber("test", (800,600), onEnd=mock_drag_done) 175 | assert not rect.isDragging 176 | 177 | mock_handler(EVENT_LBUTTONDOWN, 200, 600, "", "") 178 | assert rect.isDragging 179 | assert rect.bounds == ((200, 600), (200, 600)) 180 | 181 | mock_handler(EVENT_MOUSEMOVE, 400, 400, "", "") 182 | assert rect.bounds == ((200, 400), (400, 600)) 183 | 184 | mock_handler(EVENT_LBUTTONUP, 400, 400, "", "") 185 | 186 | def test_bl_to_tr_drag_with_aspect_ratio(mocker): 187 | mock_handler = None 188 | def mockSetClickHandler(window, handler): 189 | nonlocal mock_handler 190 | mock_handler = handler 191 | 192 | def mock_drag_done(bounds): 193 | assert bounds == ((0, 400), (400, 800)) 194 | 195 | mocker.patch( 196 | 'cv2.setMouseCallback', 197 | mockSetClickHandler 198 | ) 199 | 200 | rect = RectangleGrabber("test", (800,800), preserveAspectRatio=True, onEnd=mock_drag_done) 201 | assert not rect.isDragging 202 | 203 | mock_handler(EVENT_LBUTTONDOWN, 0, 800, "", "") 204 | assert rect.isDragging 205 | assert rect.bounds == ((0, 800), (0, 800)) 206 | 207 | mock_handler(EVENT_MOUSEMOVE, 400, 200, "", "") 208 | assert rect.bounds == ((0, 400), (400, 800)) 209 | 210 | mock_handler(EVENT_LBUTTONUP, 400, 400, "", "") -------------------------------------------------------------------------------- /tests/sleep_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from unittest.mock import patch, MagicMock, Mock 4 | from birbcam.exposureadjust.sleep import Sleep 5 | 6 | @patch('time.time', MagicMock(return_value=100)) 7 | def test_time_to_update(): 8 | def mockChangeState(state): 9 | assert state == None 10 | 11 | sleep = Sleep(50) 12 | sleep.take_over(None, None, None, mockChangeState, 100, 10) 13 | sleep.update(None, None) 14 | 15 | @patch('time.time', MagicMock(return_value=25)) 16 | def test_not_time_to_update(): 17 | mockChangeState = Mock() 18 | sleep = Sleep(50) 19 | sleep.take_over(None, None, None, mockChangeState, 100, 10) 20 | sleep.update(None, None) 21 | 22 | assert not mockChangeState.called 23 | -------------------------------------------------------------------------------- /tests/watch_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from unittest.mock import patch, MagicMock, Mock 4 | from birbcam.exposureadjust.watch import Watch 5 | from birbcam.exposureadjust.adjustup import AdjustUp 6 | from birbcam.exposureadjust.adjustdown import AdjustDown 7 | 8 | @patch('time.time', MagicMock(return_value=25)) 9 | def test_not_time_to_update(): 10 | with patch('birbcam.exposureadjust.utils.calculate_exposure', MagicMock(return_value=100)) as mock_calculate_exposure: 11 | mockChangeState = Mock() 12 | watch = Watch(50) 13 | watch.take_over(None, None, None, mockChangeState, 100, 10) 14 | watch.update(None, None) 15 | 16 | assert not mock_calculate_exposure.called 17 | 18 | @patch('time.time', MagicMock(return_value=25)) 19 | @patch('birbcam.exposureadjust.utils.calculate_exposure', MagicMock(return_value=100)) 20 | def test_no_exposure_adjustment(): 21 | mockChangeState = Mock() 22 | watch = Watch(50) 23 | watch.take_over(None, None, None, mockChangeState, 100, 10) 24 | watch.update(None, None) 25 | 26 | assert not mockChangeState.called 27 | 28 | @patch('time.time', MagicMock(return_value=25)) 29 | @patch('birbcam.exposureadjust.utils.calculate_exposure', MagicMock(return_value=80)) 30 | def test_step_up(): 31 | def mockChangeState(state): 32 | assert state.__class__.__name__ == AdjustUp.__class__.__name__ 33 | 34 | watch = Watch(50) 35 | watch.take_over(None, None, None, mockChangeState, 100, 10) 36 | watch.update(None, None) 37 | 38 | @patch('time.time', MagicMock(return_value=25)) 39 | @patch('birbcam.exposureadjust.utils.calculate_exposure', MagicMock(return_value=120)) 40 | def test_step_down(): 41 | def mockChangeState(state): 42 | assert state.__class__.__name__ == AdjustDown.__class__.__name__ 43 | 44 | watch = Watch(50) 45 | watch.take_over(None, None, None, mockChangeState, 100, 10) 46 | watch.update(None, None) --------------------------------------------------------------------------------