├── .gitignore ├── LICENSE ├── README.md ├── bin └── video_marker ├── build_pypi.sh ├── requirements.txt ├── setup.py ├── utils ├── open_logo.py └── python.png └── video_marker ├── __init__.py ├── exceptions.py ├── utils.py └── video_marker.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | output/* 4 | assets/* 5 | dist/* 6 | build/* 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | European Union Public Licence 2 | V. 1.2 3 | 4 | EUPL © the European Union 2007, 2016 5 | 6 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as 7 | defined below) which is provided under the terms of this Licence. Any use of 8 | the Work, other than as authorised under this Licence is prohibited (to the 9 | extent such use is covered by a right of the copyright holder of the Work). 10 | 11 | The Work is provided under the terms of this Licence when the Licensor (as 12 | defined below) has placed the following notice immediately following the 13 | copyright notice for the Work: “Licensed under the EUPL”, or has expressed by 14 | any other means his willingness to license under the EUPL. 15 | 16 | 1. Definitions 17 | 18 | In this Licence, the following terms have the following meaning: 19 | — ‘The Licence’: this Licence. 20 | — ‘The Original Work’: the work or software distributed or communicated by the 21 | ‘Licensor under this Licence, available as Source Code and also as 22 | ‘Executable Code as the case may be. 23 | — ‘Derivative Works’: the works or software that could be created by the 24 | ‘Licensee, based upon the Original Work or modifications thereof. This 25 | ‘Licence does not define the extent of modification or dependence on the 26 | ‘Original Work required in order to classify a work as a Derivative Work; 27 | ‘this extent is determined by copyright law applicable in the country 28 | ‘mentioned in Article 15. 29 | — ‘The Work’: the Original Work or its Derivative Works. 30 | — ‘The Source Code’: the human-readable form of the Work which is the most 31 | convenient for people to study and modify. 32 | 33 | — ‘The Executable Code’: any code which has generally been compiled and which 34 | is meant to be interpreted by a computer as a program. 35 | — ‘The Licensor’: the natural or legal person that distributes or communicates 36 | the Work under the Licence. 37 | — ‘Contributor(s)’: any natural or legal person who modifies the Work under 38 | the Licence, or otherwise contributes to the creation of a Derivative Work. 39 | — ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 40 | the Work under the terms of the Licence. 41 | — ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 42 | renting, distributing, communicating, transmitting, or otherwise making 43 | available, online or offline, copies of the Work or providing access to its 44 | essential functionalities at the disposal of any other natural or legal 45 | person. 46 | 47 | 2. Scope of the rights granted by the Licence 48 | 49 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 50 | sublicensable licence to do the following, for the duration of copyright 51 | vested in the Original Work: 52 | 53 | — use the Work in any circumstance and for all usage, 54 | — reproduce the Work, 55 | — modify the Work, and make Derivative Works based upon the Work, 56 | — communicate to the public, including the right to make available or display 57 | the Work or copies thereof to the public and perform publicly, as the case 58 | may be, the Work, 59 | — distribute the Work or copies thereof, 60 | — lend and rent the Work or copies thereof, 61 | — sublicense rights in the Work or copies thereof. 62 | 63 | Those rights can be exercised on any media, supports and formats, whether now 64 | known or later invented, as far as the applicable law permits so. 65 | 66 | In the countries where moral rights apply, the Licensor waives his right to 67 | exercise his moral right to the extent allowed by law in order to make 68 | effective the licence of the economic rights here above listed. 69 | 70 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights 71 | to any patents held by the Licensor, to the extent necessary to make use of 72 | the rights granted on the Work under this Licence. 73 | 74 | 3. Communication of the Source Code 75 | 76 | The Licensor may provide the Work either in its Source Code form, or as 77 | Executable Code. If the Work is provided as Executable Code, the Licensor 78 | provides in addition a machine-readable copy of the Source Code of the Work 79 | along with each copy of the Work that the Licensor distributes or indicates, 80 | in a notice following the copyright notice attached to the Work, a repository 81 | where the Source Code is easily and freely accessible for as long as the 82 | Licensor continues to distribute or communicate the Work. 83 | 84 | 4. Limitations on copyright 85 | 86 | Nothing in this Licence is intended to deprive the Licensee of the benefits 87 | from any exception or limitation to the exclusive rights of the rights owners 88 | in the Work, of the exhaustion of those rights or of other applicable 89 | limitations thereto. 90 | 91 | 5. Obligations of the Licensee 92 | 93 | The grant of the rights mentioned above is subject to some restrictions and 94 | obligations imposed on the Licensee. Those obligations are the following: 95 | 96 | Attribution right: The Licensee shall keep intact all copyright, patent or 97 | trademarks notices and all notices that refer to the Licence and to the 98 | disclaimer of warranties. The Licensee must include a copy of such notices and 99 | a copy of the Licence with every copy of the Work he/she distributes or 100 | communicates. The Licensee must cause any Derivative Work to carry prominent 101 | notices stating that the Work has been modified and the date of modification. 102 | 103 | Copyleft clause: If the Licensee distributes or communicates copies of the 104 | Original Works or Derivative Works, this Distribution or Communication will be 105 | done under the terms of this Licence or of a later version of this Licence 106 | unless the Original Work is expressly distributed only under this version of 107 | the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 108 | (becoming Licensor) cannot offer or impose any additional terms or conditions 109 | on the Work or Derivative Work that alter or restrict the terms of the 110 | Licence. 111 | 112 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 113 | Works or copies thereof based upon both the Work and another work licensed 114 | under a Compatible Licence, this Distribution or Communication can be done 115 | under the terms of this Compatible Licence. For the sake of this clause, 116 | ‘Compatible Licence’ refers to the licences listed in the appendix attached to 117 | this Licence. Should the Licensee's obligations under the Compatible Licence 118 | conflict with his/her obligations under this Licence, the obligations of the 119 | Compatible Licence shall prevail. 120 | 121 | Provision of Source Code: When distributing or communicating copies of the 122 | Work, the Licensee will provide a machine-readable copy of the Source Code or 123 | indicate a repository where this Source will be easily and freely available 124 | for as long as the Licensee continues to distribute or communicate the Work. 125 | 126 | Legal Protection: This Licence does not grant permission to use the trade 127 | names, trademarks, service marks, or names of the Licensor, except as required 128 | for reasonable and customary use in describing the origin of the Work and 129 | reproducing the content of the copyright notice. 130 | 131 | 6. Chain of Authorship 132 | 133 | The original Licensor warrants that the copyright in the Original Work granted 134 | hereunder is owned by him/her or licensed to him/her and that he/she has the 135 | power and authority to grant the Licence. 136 | 137 | Each Contributor warrants that the copyright in the modifications he/she 138 | brings to the Work are owned by him/her or licensed to him/her and that he/she 139 | has the power and authority to grant the Licence. 140 | 141 | Each time You accept the Licence, the original Licensor and subsequent 142 | Contributors grant You a licence to their contributions to the Work, under the 143 | terms of this Licence. 144 | 145 | 7. Disclaimer of Warranty 146 | 147 | The Work is a work in progress, which is continuously improved by numerous 148 | Contributors. It is not a finished work and may therefore contain defects or 149 | ‘bugs’ inherent to this type of development. 150 | 151 | For the above reason, the Work is provided under the Licence on an ‘as is’ 152 | basis and without warranties of any kind concerning the Work, including 153 | without limitation merchantability, fitness for a particular purpose, absence 154 | of defects or errors, accuracy, non-infringement of intellectual property 155 | rights other than copyright as stated in Article 6 of this Licence. 156 | 157 | This disclaimer of warranty is an essential part of the Licence and a 158 | condition for the grant of any rights to the Work. 159 | 160 | 8. Disclaimer of Liability 161 | 162 | Except in the cases of wilful misconduct or damages directly caused to natural 163 | persons, the Licensor will in no event be liable for any direct or indirect, 164 | material or moral, damages of any kind, arising out of the Licence or of the 165 | use of the Work, including without limitation, damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, loss of data or any commercial 167 | damage, even if the Licensor has been advised of the possibility of such 168 | damage. However, the Licensor will be liable under statutory product liability 169 | laws as far such laws apply to the Work. 170 | 171 | 9. Additional agreements 172 | 173 | While distributing the Work, You may choose to conclude an additional 174 | agreement, defining obligations or services consistent with this Licence. 175 | However, if accepting obligations, You may act only on your own behalf and on 176 | your sole responsibility, not on behalf of the original Licensor or any other 177 | Contributor, and only if You agree to indemnify, defend, and hold each 178 | Contributor harmless for any liability incurred by, or claims asserted against 179 | such Contributor by the fact You have accepted any warranty or additional 180 | liability. 181 | 182 | 10. Acceptance of the Licence 183 | 184 | The provisions of this Licence can be accepted by clicking on an icon ‘I 185 | agree’ placed under the bottom of a window displaying the text of this Licence 186 | or by affirming consent in any other similar way, in accordance with the rules 187 | of applicable law. Clicking on that icon indicates your clear and irrevocable 188 | acceptance of this Licence and all of its terms and conditions. 189 | 190 | Similarly, you irrevocably accept this Licence and all of its terms and 191 | conditions by exercising any rights granted to You by Article 2 of this 192 | Licence, such as the use of the Work, the creation by You of a Derivative Work 193 | or the Distribution or Communication by You of the Work or copies thereof. 194 | 195 | 11. Information to the public 196 | 197 | In case of any Distribution or Communication of the Work by means of 198 | electronic communication by You (for example, by offering to download the Work 199 | from a remote location) the distribution channel or media (for example, a 200 | website) must at least provide to the public the information requested by the 201 | applicable law regarding the Licensor, the Licence and the way it may be 202 | accessible, concluded, stored and reproduced by the Licensee. 203 | 204 | 12. Termination of the Licence 205 | 206 | The Licence and the rights granted hereunder will terminate automatically upon 207 | any breach by the Licensee of the terms of the Licence. Such a termination 208 | will not terminate the licences of any person who has received the Work from 209 | the Licensee under the Licence, provided such persons remain in full 210 | compliance with the Licence. 211 | 212 | 13. Miscellaneous 213 | 214 | Without prejudice of Article 9 above, the Licence represents the complete 215 | agreement between the Parties as to the Work. 216 | 217 | If any provision of the Licence is invalid or unenforceable under applicable 218 | law, this will not affect the validity or enforceability of the Licence as a 219 | whole. Such provision will be construed or reformed so as necessary to make it 220 | valid and enforceable. 221 | 222 | The European Commission may publish other linguistic versions or new versions 223 | of this Licence or updated versions of the Appendix, so far this is required 224 | and reasonable, without reducing the scope of the rights granted by the 225 | Licence. New versions of the Licence will be published with a unique version 226 | number. 227 | 228 | All linguistic versions of this Licence, approved by the European Commission, 229 | have identical value. Parties can take advantage of the linguistic version of 230 | their choice. 231 | 232 | 14. Jurisdiction 233 | 234 | Without prejudice to specific agreement between parties, 235 | — any litigation resulting from the interpretation of this License, arising 236 | between the European Union institutions, bodies, offices or agencies, as a 237 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 238 | of Justice of the European Union, as laid down in article 272 of the Treaty 239 | on the Functioning of the European Union, 240 | — any litigation arising between other parties and resulting from the 241 | interpretation of this License, will be subject to the exclusive 242 | jurisdiction of the competent court where the Licensor resides or conducts 243 | its primary business. 244 | 245 | 15. Applicable Law 246 | 247 | Without prejudice to specific agreement between parties, 248 | — this Licence shall be governed by the law of the European Union Member State 249 | where the Licensor has his seat, resides or has his registered office, 250 | — this licence shall be governed by Belgian law if the Licensor has no seat, 251 | residence or registered office inside a European Union Member State. 252 | 253 | Appendix 254 | 255 | ‘Compatible Licences’ according to Article 5 EUPL are: 256 | — GNU General Public License (GPL) v. 2, v. 3 257 | — GNU Affero General Public License (AGPL) v. 3 258 | — Open Software License (OSL) v. 2.1, v. 3.0 259 | — Eclipse Public License (EPL) v. 1.0 260 | — CeCILL v. 2.0, v. 2.1 261 | — Mozilla Public Licence (MPL) v. 2 262 | — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 263 | — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 264 | works other than software 265 | — European Union Public Licence (EUPL) v. 1.1, v. 1.2 266 | — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or 267 | Strong Reciprocity (LiLiQ-R+) 268 | 269 | — The European Commission may update this Appendix to later versions of the 270 | above licences without producing a new version of the EUPL, as long as they 271 | provide the rights granted in Article 2 of this Licence and protect the 272 | covered Source Code from exclusive appropriation. 273 | — All other changes or additions to this Appendix require the production of a 274 | new EUPL version. 275 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Video Marker 2 | ------------ 3 | 4 | Video Marker is a python application based on OpenCV that can automate the following tasks: 5 | 6 | - video recording from a webcam or any other OpenVC VideoCapture source 7 | - prepend a video before your video record starts 8 | - append a video after your video record ends 9 | - add a watermark image 10 | - save to a file 11 | 12 | ## Setup 13 | 14 | ```` 15 | pip install video-marker 16 | ```` 17 | 18 | ## Documentation 19 | ```` 20 | video_marker -h 21 | ```` 22 | 23 | ## Usage examples 24 | 25 | Monitor your webcam, press `q` key on the keyboard or `CTRL+c` 26 | ```` 27 | video_marker --video-cap 0 --monitor 28 | ```` 29 | 30 | Add watermark to your webcam 31 | ```` 32 | video_marker --watermark-fpath 'assets/logo.png' --monitor 33 | ```` 34 | 35 | A quite complete example 36 | ```` 37 | video_marker --watermark-fpath 'assets/logo.png' --monitor --pre-media assets/intro.mp4 --post-media assets/intro.mp4 --watermark-size 50 38 | ```` 39 | 40 | You can stop the video recording gracefully, with `SIGTERM` signal `kill -SIGTERM $(ps a | grep video_marker | grep python | awk -F' ' {'print $1'} | xargs kill -SIGTERM)`. 41 | 42 | 43 | ## License 44 | 45 | ses [LICENSE](LICENSE) file. 46 | 47 | ## Author 48 | 49 | Giuseppe De Marco 50 | -------------------------------------------------------------------------------- /bin/video_marker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import logging 4 | import sys 5 | 6 | from video_marker import __version__ 7 | from video_marker.video_marker import VideoMarker 8 | from video_marker.utils import print_available_codecs 9 | 10 | 11 | parser = argparse.ArgumentParser( 12 | description=f"{__file__} prepends, appends and adds a watermark to your live video recording.", 13 | epilog=f"{__file__} arguments", 14 | formatter_class=argparse.RawTextHelpFormatter 15 | ) 16 | parser.add_argument( 17 | '--video-cap', 18 | required=False, 19 | default="0", 20 | help=("select a video source, 0 means your first camera device. You can even use a video image path.") 21 | ) 22 | parser.add_argument( 23 | '--resolution', 24 | required=False, 25 | default='720p', 26 | choices = ['360p', '480p', '720p', '1080p', '4k'], 27 | help=("video recording resolution") 28 | ) 29 | parser.add_argument( 30 | '--save-path', 31 | required=False, 32 | default='output/output.avi', 33 | help=("where your video recording file will be saved") 34 | ) 35 | parser.add_argument( 36 | '--frames-per-seconds', 37 | required=False, default=50, 38 | type=int, 39 | help=("FPS") 40 | ) 41 | parser.add_argument( 42 | '--watermark-fpath', 43 | required=False, 44 | help=("image file to add as watermark") 45 | ) 46 | parser.add_argument( 47 | '--watermark-inverted', 48 | action="store_true", 49 | default=False, 50 | help=("invert watermak image") 51 | ) 52 | parser.add_argument( 53 | '--watermark-size', 54 | type=int, 55 | required=False, 56 | help=("watermark size") 57 | ) 58 | parser.add_argument( 59 | '--pre-media', 60 | required=False, 61 | help=("example: assets/intro.mp4") 62 | ) 63 | parser.add_argument( 64 | '--post-media', 65 | required=False, 66 | help=("example: assets/intro.mp4") 67 | ) 68 | parser.add_argument( 69 | '--slow-down-by', 70 | required=False, default=0, 71 | type=int, 72 | help=("how much the recording video will be slowed") 73 | ) 74 | parser.add_argument( 75 | '--exception-grace-period', 76 | required=False, default=0.3, 77 | type=float, 78 | help=("time sleep if an exception happens reading " 79 | "the VideoSource during recording" 80 | ) 81 | ) 82 | parser.add_argument( 83 | '--max-recording-exceptions', 84 | required=False, default=4, 85 | type=int, 86 | help=("how many times the recorder retries the " 87 | "frame acquisition from Video device" 88 | ) 89 | ) 90 | parser.add_argument( 91 | '--monitor', 92 | action="store_true", 93 | default=False, 94 | help=("display a recording monitor for debug purpose ") 95 | ) 96 | parser.add_argument( 97 | '-d', '--debug', required=False, 98 | choices=('CRITICAL', 'ERROR', 99 | 'WARNING', 'INFO', 'DEBUG'), 100 | default='INFO', 101 | help="Debug level, see python logging; defaults to INFO if omitted" 102 | ) 103 | parser.add_argument( 104 | '-v', '--version', required=False, 105 | action="store_true", 106 | help="Print version and exit" 107 | ) 108 | parser.add_argument( 109 | '-sc', '--show-codecs', required=False, 110 | action="store_true", 111 | help="Print all the available codecs" 112 | ) 113 | parser.add_argument( 114 | '--flip', 115 | required=False, default=0, 116 | type=int, 117 | choices=(0,1,-1), 118 | help="Flip image,: 0, 1 or -1" 119 | ) 120 | 121 | _args = parser.parse_args() 122 | logging.basicConfig(level=getattr(logging, _args.debug)) 123 | 124 | if _args.version: 125 | sys.exit(f'{__version__}') 126 | elif _args.show_codecs: 127 | print_available_codecs() 128 | sys.exit() 129 | 130 | vm = VideoMarker(**_args.__dict__) 131 | vm.start() 132 | -------------------------------------------------------------------------------- /build_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJ_NAME=$(ls | grep *.egg-info | sed -e 's/.egg-info//g') ; rm -R build/ dist/* *.egg-info ; pip uninstall $PROJ_NAME ; python setup.py build sdist 4 | twine upload dist/* 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python>=4.5,<4.6 2 | numpy 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from glob import glob 4 | from setuptools import setup 5 | 6 | def readme(): 7 | with open('README.md') as f: 8 | return f.read() 9 | 10 | _pkg_name = 'video_marker' 11 | 12 | with open(f'{_pkg_name}/__init__.py', 'r') as fd: 13 | VERSION = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 14 | fd.read(), re.MULTILINE).group(1) 15 | 16 | setup( 17 | name=_pkg_name, 18 | version=VERSION, 19 | description="OpenCV based video Marker utility", 20 | long_description=readme(), 21 | long_description_content_type='text/markdown', 22 | classifiers=['Development Status :: 5 - Production/Stable', 23 | 'License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)', 24 | 'Programming Language :: Python :: 3'], 25 | url='https://github.com/peppelinux/video-marker', 26 | author='Giuseppe De Marco', 27 | author_email='demarcog83@gmail.com', 28 | license='License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)', 29 | scripts=[f'bin/{_pkg_name}'], 30 | packages=[f"{_pkg_name}"], 31 | package_dir={f"{_pkg_name}": f"{_pkg_name}"}, 32 | package_data={f"{_pkg_name}": [ 33 | i.replace(f'{_pkg_name}/', '') 34 | for i in glob(f'{_pkg_name}/**', recursive=True) 35 | ] 36 | }, 37 | install_requires=[ 38 | 'opencv-python>=4.5,<4.6', 39 | 'numpy' 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /utils/open_logo.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | file_name = "python.png" 3 | 4 | src = cv2.imread(file_name, cv2.IMREAD_UNCHANGED) 5 | 6 | # Save the transparency channel alpha 7 | *_, alpha = cv2.split(src) 8 | 9 | gray_layer = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) 10 | 11 | # ... Your image processing 12 | 13 | # Duplicate the grayscale image to mimic the BGR image and finally add the transparency 14 | dst = cv2.merge((gray_layer, gray_layer, gray_layer, alpha)) 15 | #cv2.imwrite("result.png", dst) 16 | 17 | while 1: 18 | cv2.imshow('watermark',dst) 19 | if cv2.waitKey(20) & 0xFF == ord('q'): 20 | break 21 | -------------------------------------------------------------------------------- /utils/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peppelinux/video-marker/a07c0ff3bd7fe0bc66d31bb0b2c0b8562165cd47/utils/python.png -------------------------------------------------------------------------------- /video_marker/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import signal 3 | import sys 4 | import logging 5 | 6 | __version__ = "0.7.1" 7 | logger = logging.getLogger(__name__) 8 | -------------------------------------------------------------------------------- /video_marker/exceptions.py: -------------------------------------------------------------------------------- 1 | class MissingFrame(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /video_marker/utils.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import os 3 | 4 | # source: https://stackoverflow.com/a/44659589 5 | def image_resize(image, width = None, height = None, inter = cv2.INTER_AREA): 6 | # initialize the dimensions of the image to be resized and 7 | # grab the image size 8 | dim = None 9 | (h, w) = image.shape[:2] 10 | # if both the width and height are None, then return the 11 | # original image 12 | if width is None and height is None: 13 | return image 14 | # check to see if the width is None 15 | if width is None: 16 | # calculate the ratio of the height and construct the 17 | # dimensions 18 | r = height / float(h) 19 | dim = (int(w * r), height) 20 | # otherwise, the height is None 21 | else: 22 | # calculate the ratio of the width and construct the 23 | # dimensions 24 | r = width / float(w) 25 | dim = (width, int(h * r)) 26 | 27 | # resize the image 28 | resized = cv2.resize(image, dim, interpolation = inter) 29 | # return the resized image 30 | return resized 31 | 32 | 33 | 34 | class CFEVideoConf(object): 35 | # Standard Video Dimensions Sizes 36 | STD_DIMENSIONS = { 37 | "360p": (480, 360), 38 | "480p": (640, 480), 39 | "720p": (1280, 720), 40 | "1080p": (1920, 1080), 41 | "4k": (3840, 2160), 42 | } 43 | # Video Encoding, might require additional installs 44 | # Types of Codes: http://www.fourcc.org/codecs.php 45 | VIDEO_TYPE = { 46 | 'avi': cv2.VideoWriter_fourcc(*'XVID'), 47 | #'avi': cv2.VideoWriter_fourcc(*'X264'), 48 | 'mjpeg': cv2.VideoWriter_fourcc(*'MJPG'), 49 | 'mp4': cv2.VideoWriter_fourcc(*'mp4v') 50 | } 51 | 52 | width = 640 53 | height = 480 54 | dims = (640, 480) 55 | capture = None 56 | video_type = None 57 | def __init__(self, capture, filepath, res="480p", *args, **kwargs): 58 | self.capture = capture 59 | self.filepath = filepath 60 | self.width, self.height = self.get_dims(res=res) 61 | self.video_type = self.get_video_type() 62 | 63 | # Set resolution for the video capture 64 | # Function adapted from https://kirr.co/0l6qmh 65 | def change_res(self, width, height): 66 | self.capture.set(3, width) 67 | self.capture.set(4, height) 68 | 69 | def get_dims(self, res='480p'): 70 | width, height = self.STD_DIMENSIONS['480p'] 71 | if res in self.STD_DIMENSIONS: 72 | width, height = self.STD_DIMENSIONS[res] 73 | self.change_res(width, height) 74 | self.dims = (width, height) 75 | return width, height 76 | 77 | def get_video_type(self): 78 | ext = self.filepath.rpartition('.')[-1] 79 | if ext in self.VIDEO_TYPE: 80 | return self.VIDEO_TYPE[ext] 81 | return self.VIDEO_TYPE['avi'] 82 | 83 | 84 | def print_available_codecs(): 85 | cv2.VideoWriter('output.mp4', -1, 12,(640,480)) 86 | -------------------------------------------------------------------------------- /video_marker/video_marker.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import logging 3 | import numpy as np 4 | import os 5 | import signal 6 | import time 7 | 8 | from .exceptions import MissingFrame 9 | from . utils import CFEVideoConf, image_resize 10 | 11 | logger = logging.getLogger(__name__) 12 | _WINDOW_NAME = 'monitor' 13 | 14 | 15 | class VideoMarker(object): 16 | VIDEO_RES = '720p' 17 | 18 | def __init__(self, 19 | video_cap:str = "0", 20 | resolution:str = VIDEO_RES, 21 | save_path = 'output/output.avi', 22 | frames_per_seconds = 50, 23 | watermark_fpath = 'assets/logo.png', 24 | pre_media = 'assets/intro.mp4', 25 | post_media = 'assets/intro.mp4', 26 | watermark_size = 50, 27 | watermark_inverted = False, 28 | slow_down_by = 0, 29 | exception_grace_period = 0.2, 30 | max_recording_exceptions = 4, 31 | flip = 0, 32 | monitor = False, **kwargs 33 | ): 34 | self.video_source = int(video_cap) if video_cap.isdigit() else video_cap 35 | self.video_size = CFEVideoConf.STD_DIMENSIONS[resolution] 36 | 37 | self.slow_down_by = slow_down_by 38 | 39 | self.watermark_fpath = watermark_fpath 40 | self.watermark_size = watermark_size 41 | self.watermark_inverted = watermark_inverted 42 | self.pre_media = pre_media 43 | self.post_media = post_media 44 | self.monitor = monitor 45 | 46 | self.save_path = save_path 47 | self.frames_per_seconds = frames_per_seconds 48 | 49 | self.video_writer_config_params = dict( 50 | res = resolution, 51 | filepath = save_path 52 | ) 53 | 54 | self.state = 0 55 | 56 | self.exception_grace_period = exception_grace_period 57 | self.max_recording_exceptions = max_recording_exceptions 58 | self.exceptions_counter = 0 59 | self.flip = flip 60 | 61 | self._load_video_source() 62 | 63 | def release_device(self): 64 | self.capture.release() 65 | self.video_writer.release() 66 | 67 | def _load_video_source(self): 68 | """ 69 | Load and reload a video source runtime 70 | """ 71 | if hasattr(self, 'capture'): 72 | del(self.capture) 73 | if hasattr(self, 'video_writer_config'): 74 | del(self.video_writer_config) 75 | del(self.video_writer) 76 | 77 | # do not overwrite existing recordings 78 | fpath = self.video_writer_config_params['filepath'] 79 | while os.path.exists(fpath): 80 | logger.warning( 81 | f"Found an existing video in {fpath}, trying to not overwrite it" 82 | ) 83 | exploded_path = list(fpath.rpartition('/')) 84 | exploded_path.insert(-1, '_') 85 | fpath = ''.join(exploded_path) 86 | self.save_path = fpath 87 | 88 | logger.info(f"Writing video to {fpath}") 89 | self.video_writer_config_params['filepath'] = fpath 90 | 91 | self.capture = cv2.VideoCapture(self.video_source) 92 | cam_fps = self.capture.get(cv2.CAP_PROP_FPS) 93 | logger.info( 94 | f"Your camera has {cam_fps} FPS, you choose to record {self.frames_per_seconds} fps" 95 | ) 96 | self.capture.set(cv2.CAP_PROP_FPS, self.frames_per_seconds) 97 | self.video_writer_config = CFEVideoConf( 98 | self.capture, **self.video_writer_config_params 99 | ) 100 | 101 | self.video_writer = cv2.VideoWriter( 102 | self.save_path, self.video_writer_config.video_type, 103 | self.frames_per_seconds, self.video_writer_config.dims, 104 | ) 105 | 106 | @staticmethod 107 | def _assert_frame(frame): 108 | if not hasattr(frame, 'any') or not frame.any(): 109 | return False 110 | else: 111 | return True 112 | 113 | def get_watermark(self): 114 | if not self.watermark_fpath: 115 | return 116 | logo = cv2.imread(self.watermark_fpath, cv2.IMREAD_UNCHANGED) 117 | if logo.shape[2] == 4: # we have an alpha channel 118 | a1 = ~logo[:,:,3] # extract and invert that alpha 119 | logo = cv2.add(cv2.merge([a1,a1,a1,a1]), logo) # add up values (with clipping) 120 | logo = cv2.cvtColor(logo, cv2.COLOR_RGBA2RGB) # strip alpha channel 121 | 122 | alpha = logo[:,:,1] # Channel 3 123 | result = np.dstack([logo, alpha]) # Add the alpha channel 124 | 125 | if self.watermark_inverted: 126 | # invert the array 127 | result = np.invert(result) 128 | 129 | watermark = image_resize(result, height=self.watermark_size) 130 | watermark = cv2.cvtColor(watermark, cv2.COLOR_BGR2BGRA) 131 | return watermark 132 | 133 | def pre_post_video(self, media:str): 134 | if not media or not os.path.isfile(media): 135 | return 136 | 137 | intro = cv2.VideoCapture(media) 138 | while 1: 139 | ret, frame = intro.read() 140 | if ret: 141 | # not needed ... 142 | # frame = cv2.cvtColor(frame, cv2.COLOR_BGR2BGRA) 143 | # frame = cv2.resize(frame, VIDEO_SIZE) 144 | self.video_writer.write(frame) 145 | 146 | if self.monitor: 147 | cv2.imshow(_WINDOW_NAME, frame) 148 | if cv2.waitKey(20) & 0xFF == ord('q'): 149 | break 150 | else: 151 | break 152 | intro.release() 153 | 154 | def _assert_videodevice(self): 155 | # frame sample 156 | ret, frame = self.capture.read() 157 | if not self._assert_frame(frame): 158 | logger.critical("Missing frame in .capture.read() ... exit") 159 | raise MissingFrame("OpenCV frame is None, check video device") 160 | 161 | 162 | def record_video(self, watermark = None): 163 | if isinstance(watermark, np.ndarray): 164 | watermark_h, watermark_w, watermark_c = watermark.shape 165 | # replace overlay pixels with watermark pixel values 166 | h_range = range(0, watermark_h) 167 | w_range = range(0, watermark_w) 168 | _add_watermark = True 169 | else: 170 | _add_watermark = False 171 | 172 | slow_down_by = range(self.slow_down_by - 1) 173 | 174 | # optimizations 175 | cvtColor = cv2.cvtColor 176 | zeros = np.zeros 177 | addWeighted = cv2.addWeighted 178 | cv2_resize = cv2.resize 179 | COLOR_BGRA2BGR = cv2.COLOR_BGRA2BGR 180 | COLOR_BGR2BGRA = cv2.COLOR_BGR2BGRA 181 | 182 | ret, frame = self.capture.read() 183 | frame = cvtColor(frame, cv2.COLOR_BGR2BGRA) 184 | 185 | frame_h, frame_w, frame_c = frame.shape 186 | # overlay with 4 channels BGR and Alpha 187 | overlay = zeros((frame_h, frame_w, 4), dtype='uint8') 188 | 189 | if _add_watermark: 190 | for i in h_range: 191 | for j in w_range: 192 | offset = 10 193 | h_offset = frame_h - watermark_h - offset 194 | w_offset = frame_w - watermark_w - offset 195 | overlay[h_offset + i, w_offset+ j] = watermark[i,j] 196 | 197 | while (self.state >= 0): 198 | if self.exceptions_counter >=self.max_recording_exceptions: 199 | excp_msg = ( 200 | "Exception during video recording with " 201 | f"exceptions_counter={self.exceptions_counter} " 202 | f"and exception_grace_period={self.exception_grace_period}" 203 | ) 204 | logger.critical(excp_msg) 205 | raise MissingFrame( 206 | excp_msg 207 | ) 208 | 209 | # Capture frame-by-frame 210 | ret, frame = self.capture.read() 211 | if not self._assert_frame(frame): 212 | self.exceptions_counter += 1 213 | time.sleep(self.exception_grace_period) 214 | self._load_video_source() 215 | continue 216 | else: 217 | self.exceptions_counter = 0 218 | 219 | frame = cvtColor(frame, COLOR_BGR2BGRA) 220 | 221 | if _add_watermark: 222 | addWeighted(overlay, 0.25, frame, 1.0, 0, frame) 223 | 224 | frame = cvtColor(frame, COLOR_BGRA2BGR) 225 | if self.flip: 226 | frame = cv2.flip(frame, self.flip) 227 | resized = cv2_resize(frame, self.video_size) 228 | 229 | self.video_writer.write(resized) 230 | for n in slow_down_by: 231 | self.video_writer.write(resized) 232 | 233 | if self.monitor: 234 | # Display the resulting frame 235 | cv2.imshow(_WINDOW_NAME, resized) 236 | if cv2.waitKey(20) & 0xFF == ord('q'): 237 | break 238 | 239 | def repr_params(self): 240 | return '\n'.join([f"{k}: {v}" for k,v in self.__dict__.items()]) 241 | 242 | def signal_handler(self, sig, frame): 243 | logger.info('INT Signal received, shutting down') 244 | self.state = -1 245 | # sys.exit(0) 246 | 247 | def start(self): 248 | self.state = 1 249 | signal.signal(signal.SIGTERM, self.signal_handler) 250 | logger.info(f"Starting video recording with {self.repr_params()}") 251 | self._assert_videodevice() 252 | self.pre_post_video(media = self.pre_media) 253 | _watermark = self.get_watermark() 254 | self.record_video(watermark = _watermark) 255 | self.pre_post_video(media = self.post_media) 256 | 257 | # When everything done, release the capture 258 | self.release_device() 259 | cv2.destroyAllWindows() 260 | --------------------------------------------------------------------------------