├── .gitignore ├── LICENSE ├── README.md ├── examples ├── Big_Buck_Bunny_1080p.png ├── code_example.png ├── column_example.png ├── grid_example.png └── small_example.png ├── pyVideoSheet ├── Cabin-Regular-TTF.ttf ├── __init__.py └── create.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | 5 | # Icon must end with two \r 6 | Icon 7 | 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | 20 | # Directories potentially created on remote AFP share 21 | .AppleDB 22 | .AppleDesktop 23 | Network Trash Folder 24 | Temporary Items 25 | .apdisk 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | 364 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyVideoSheet 2 | Python video thumbnail contact sheet creator. 3 | 4 | pyVideoSheet is designed for efficiency and simplicity, allowing users to create video contact sheets rapidly. 5 | pyVideoSheet can be used as a standalong command-line application and as an importable package. 6 | 7 | ![title](https://raw.githubusercontent.com/rorasa/pyVideoSheet/master/examples/small_example.png) 8 | 9 | ## Dependencies 10 | 11 | pyVideoSheet requires the following softwares and packages to work: 12 | 13 | * Python (Of course! pyVideoSheet is written in Python 2.7 though) 14 | * Python Image Library (PIL) 15 | * FFmpeg 16 | 17 | ### Basic instruction on how to install dependencies 18 | 19 | #### Python 20 | Because pyVideoSheet is written in Python, therefore there must be a valid Python installed to use pyVideoSheet. 21 | 22 | Python package for any platform can be downloaded from https://www.python.org/downloads/ 23 | 24 | #### Python Image Library (PIL) 25 | There are many ways to install PIL, but here we would recommend to use [Pillow](https://pypi.python.org/pypi/Pillow/3.0.0), a nice and easy folk of PIL. 26 | To install Pillow, run this command 27 | ``` 28 | easy_install Pillow 29 | ``` 30 | More details instruction on how to install Pillow can be found in Pillow's [documentation](https://pillow.readthedocs.org/en/3.0.x/index.html). 31 | 32 | #### FFmpeg 33 | FFmpeg is a cross-platform media solution that pyVideoSheet uses as the primary way to interact with a video. 34 | pyVideoSheet requires a working FFmpeg to work. 35 | 36 | You can get the latest FFmpeg from its website https://www.ffmpeg.org/download.html . 37 | Because compiling FFmpeg properly is quite tricky, we'd recommend you to stick to the pre-built packages unless you really need otherwise. 38 | 39 | Please note that FFmpeg must be included in the system search path for pyVideoSheet to work correctly. 40 | You can test this by running 41 | ``` 42 | ffmpeg -h 43 | ``` 44 | If you can see FFmpeg's help, it should work alright. Otherwise please check how to add FFmpeg to search path in its [documentations](https://www.ffmpeg.org/documentation.html) 45 | 46 | ## Installing pyVideoSheet 47 | 48 | 1. Get the latest version of pyVideoSheet. 49 | This can be done by either using git: 50 | 51 | ``` 52 | git clone https://github.com/rorasa/pyVideoSheet.git 53 | ``` 54 | 55 | or by downloading the zip archive https://github.com/rorasa/pyVideoSheet/archive/master.zip and extract it. 56 | 2. Go into the downloaded directory 57 | ``` 58 | cd pyVideoSheet- 59 | ``` 60 | 3. Install the package 61 | ``` 62 | python setup.py install 63 | ``` 64 | 65 | ## Using pyVideoSheet as a standalone application 66 | 67 | pyVideoSheet can be used as a command-line application. This provides a quick and easy way to create contact sheets. 68 | The best thing about being a command-line application, as opposed to a GUI one, is the ease of writing a batch script for it, especially when one has to work with lots of videos. 69 | 70 | Let's see how to create a contact sheet for a video. 71 | In this readme we're going to make contact sheets for this lovely creative common film [**Big Buck Bunny**](https://peach.blender.org/) by [Peach project](https://peach.blender.org/). 72 | To make a contact sheet, run this command 73 | ``` 74 | python -m pyVideoSheet.create Big_Buck_Bunny_1080p.avi 75 | ``` 76 | and you should get this result. 77 | ![example sheet](https://raw.githubusercontent.com/rorasa/pyVideoSheet/master/examples/Big_Buck_Bunny_1080p.png) 78 | This executes *create* utility of pyVideoSheet to create a contact sheet with the same name as our video using default options. 79 | Customisation options can be added using optional flags. 80 | 81 | There are many customisation options, as follows: 82 | 83 | Options | Example | Description 84 | --------|---------|------------- 85 | -h, --help | --help | Display pyVideoSheet's instruction on command-line 86 | -o, --output | -o out.png | Specify contact sheet's filename 87 | -n, --number | -n 30 | Specify total number of thumbnails. Each thumbnail is at equidistant apart. The default option is -n 20. This option cannot be use with --interval. 88 | -i, --interval | -i 300 | Specify fixed interval between each thumbnails. Total number of thumbnail is vary. This option cannot be use with --number. 89 | -c, --column | -c 6 | Specify number of column of the thumbnail grid. The default is -c 5. 90 | --notime | --notime | Remove thumbnail's timestamp. 91 | --header | --header 120 | Specify the height of description header in pixels. The default is 100. 92 | -t, --thumbsize | -t 300 250 | Specify the maximum size of thumbnail. Thumbnails will retain its aspect ratio. The default is -t 220 220. 93 | --textcolour | --textcolour 255 255 0 0 | Specify description's text colour in RGBA format. 94 | --bgcolour | --bgcolour 255 255 0 0 | Specify description's text colour in RGBA format. 95 | --font | --font font-file.ttf 12 | Specify description's font and font size. Support any TrueType font. 96 | --preview | --preview | Preview the contact sheet on default image viewer. 97 | 98 | ## Using pyVideoSheet as a package 99 | 100 | pyVideoSheet can be imported into any Python project, allowing a quick and flexible way to create a video contact sheet programatically. 101 | 102 | ### Examples 103 | First example shows how to create a simple contact sheet for *Big_Buck_Bunny_1080p.avi* 104 | ```python 105 | import pyVideoSheet as pvs 106 | 107 | vid = pvs.Video("Big_Buck_Bunny_1080p.avi") 108 | vsheet = pvs.Sheet(vid) 109 | vsheet.makeSheetByNumber(20) 110 | vsheet.sheet.save("code_example.png") 111 | ``` 112 | This results in a default contact sheet. 113 | ![code example png](https://raw.githubusercontent.com/rorasa/pyVideoSheet/master/examples/code_example.png) 114 | 115 | The next example shows the full customisation capability of pyVideoSheet. 116 | ```python 117 | import pyVideoSheet as pvs 118 | 119 | vid = pvs.Video("Big_Buck_Bunny_1080p.avi") # create Video object 120 | vidLength = vid.getVideoDuration() # get video duration in seconds 121 | 122 | # create a 5x6 grid contact sheet with blue text without timestamp 123 | vsheet1 = pvs.Sheet(vid) # create Sheet object 124 | vsheet1.setProperty('gridColumn',6) # set 6 columns 125 | vsheet1.setProperty('textColour',(0,0,255,0)) # set text to blue 126 | vsheet1.setProperty('timestamp',False) # disable timestamp 127 | 128 | sheet_1 = vsheet1.makeSheetByNumber(30) # create contact sheet of 5 times 6 columns 129 | sheet_1.save('grid_example.png') # save with PIL save() 130 | 131 | vsheet1.sheet.show() # preview with PIL show(). The created sheet is kept as Sheet.sheet variable. 132 | 133 | # create a single row preview with black text on white background 134 | vsheet2 = pvs.Sheet(vid) # create Sheet object 135 | vsheet2.setProperty('gridColumn',1) # set 1 columns 136 | vsheet2.setProperty('textColour',(0,0,0,0)) # set text to black 137 | vsheet2.setProperty('backgroundColour',(255,255,255,0)) # set background to white 138 | vsheet2.setProperty('maxThumbSize',(500,500)) # allow larger thumbnails 139 | 140 | sheet_2 = vsheet2.makeSheetByInterval(600) # create one thumbnail every 10 minutes 141 | sheet_2.show() 142 | 143 | vsheet2.sheet.save('column_example.png') # save with PIL save() 144 | ``` 145 | This is our sheet_1 "grid_example.png" 146 | ![grid example png](https://raw.githubusercontent.com/rorasa/pyVideoSheet/master/examples/grid_example.png) 147 | 148 | And this is our sheet_2 "column_example.png" 149 | 150 | ![column example png](https://raw.githubusercontent.com/rorasa/pyVideoSheet/master/examples/column_example.png) 151 | 152 | 153 | ### Video class 154 | 155 | Video class represents a video object. 156 | It contains information regarding each specific video as well as providing low level interaction between Python and video. 157 | 158 | ####Video class functions 159 | 160 | **Video(*file*)** 161 | 162 | Constructor of Video class. 163 | - **Parameter**: **file**—a file name string of video (including its path). 164 | - **Return**: A Video object. 165 | 166 | **getFileSize()** 167 | 168 | Get file size of the video in MB. 169 | - **Return**: File size in MB. 170 | 171 | **getVideoDuration()** 172 | 173 | Get duration of the video in seconds. 174 | - **Return**: Duration in seconds. 175 | 176 | **getFrameAt(*seektime*)** 177 | 178 | Capture a frame at *seektime* seconds from the beginning. 179 | - **Parameter**: **seektime**—time in seconds. 180 | - **Return**: A PIL Image object. 181 | 182 | **makeThumbnails(*interval*)** 183 | 184 | Create a list of frames captured at every fixed *interval*. 185 | - **Parameter**: **interval**—time in seconds. 186 | - **Return**: A List of PIL Image objects. 187 | 188 | **shrinkThumbs(*maxSize*)** 189 | 190 | Reduce the resolution of Video.thumbnails. 191 | - **Parameter**: **maxSize**—a tuple of (maxWidth, maxHeight). 192 | - **Return**: Video.thumbnails. 193 | 194 | ### Sheet class 195 | 196 | Sheet class handles the creation of contact sheet. 197 | Contact sheet's customisation can be done by setting Sheet class's properties. 198 | 199 | ####Sheet class functions 200 | 201 | **Sheet(*Video*)** 202 | 203 | Constructor of Sheet class. 204 | - **Parameter**: **Video**—a Video object. 205 | - **Return**: A Sheet object. 206 | 207 | **setProperty(*prop*,*value*)** 208 | 209 | Set customisation options of Sheet. 210 | - **Parameter**: **prop**—a property string. 211 | **value**—a property value 212 | 213 | Property string | Default Property Value | Description 214 | ----------------|----------------|------------- 215 | 'font' | ('Cabin-Regular-TTF.ttf', 15) | Set description's font and font size. Takes value as a tuple of font file name (string) and font size (integer). 216 | 'backgroundColour' | (0,0,0,0) | Set header's background colour. Takes value as a tuple containing 4 value for RGBA colour. 217 | 'textColour' | (255,255,255,0) | Set description's text colour. Takes value as a tuple containing 4 value for RGBA colour. 218 | 'headerSize' | 100 | Set header's height in pixels. 219 | 'gridColumn' | 5 | Set number of thumbnail columns in the grid. 220 | 'maxThumbSize' | (220,220) | Set maximum width and height of each thumbnail. The thumbnail will always retain its aspect ratio. Takes value as a tuple of max width and max height. 221 | 'timestamp' | True | Enable thumbnail timestamp. Takes boolean as its value. 222 | 223 | **makeSheetByInterval(*interval*)** 224 | 225 | Create a contact sheet. Each thumbnail is created at fixed *interval*. 226 | The total number of thumbnails is vary by the video duration divided by *interval*. 227 | - **Parameter**: **interval**—a time in seconds. 228 | - **Return**: A PIL Image object. 229 | 230 | **makeSheetByNumber(*number*)** 231 | 232 | Create a contact sheet. The total number of thumbnails is fixed. 233 | Each thumbnail is created at the interval defined by the video duration divided by *number*. 234 | - **Parameter**: **number**—a number of thumbnails. 235 | - **Return**: A PIL Image object. 236 | 237 | ## Supported formats 238 | 239 | pyVideoSheet works based upon PIL and FFmpeg, thus the supporting formats are set by these dependencies. 240 | Most commonly used video formats (such as mp4, avi, mov *etc.*) should work with FFmpeg, thus should work with pyVideoSheet. 241 | Most encoding should be supported as well (including MPEG-2, Xvid, h264). 242 | For more information on FFmpeg supported formats, please check [FFmpeg documentation](https://www.ffmpeg.org/documentation.html) 243 | Most image formats (including JPEG and PNG) should also work as an output format. 244 | 245 | ## License and development information 246 | ### License 247 | pyVideoSheet source, including this document, is distributed under Mozilla Public License 2.0. Please refer to LICENSE file for information. 248 | 249 | ### Developer 250 | pyVideoSheet is created and maintained by [Wattanit Hotrakool](https://github.com/rorasa). 251 | You can reach the developer directly through [twitter](https://www.twitter.com/rorasa). 252 | 253 | ### Development log 254 | 255 | 30 October 2015 256 | - Converted into a python package with setup script. 257 | 258 | 28 October 2015 259 | - Converted into a executable module (legacy) 260 | 261 | 27 October 2015 262 | - First working version 263 | -------------------------------------------------------------------------------- /examples/Big_Buck_Bunny_1080p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rorasa/pyVideoSheet/dd02860ae0c45da621f8d6d9e0fcf39332d016d4/examples/Big_Buck_Bunny_1080p.png -------------------------------------------------------------------------------- /examples/code_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rorasa/pyVideoSheet/dd02860ae0c45da621f8d6d9e0fcf39332d016d4/examples/code_example.png -------------------------------------------------------------------------------- /examples/column_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rorasa/pyVideoSheet/dd02860ae0c45da621f8d6d9e0fcf39332d016d4/examples/column_example.png -------------------------------------------------------------------------------- /examples/grid_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rorasa/pyVideoSheet/dd02860ae0c45da621f8d6d9e0fcf39332d016d4/examples/grid_example.png -------------------------------------------------------------------------------- /examples/small_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rorasa/pyVideoSheet/dd02860ae0c45da621f8d6d9e0fcf39332d016d4/examples/small_example.png -------------------------------------------------------------------------------- /pyVideoSheet/Cabin-Regular-TTF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rorasa/pyVideoSheet/dd02860ae0c45da621f8d6d9e0fcf39332d016d4/pyVideoSheet/Cabin-Regular-TTF.ttf -------------------------------------------------------------------------------- /pyVideoSheet/__init__.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE, STDOUT 2 | from PIL import Image, ImageDraw, ImageFont 3 | import StringIO 4 | import re 5 | import os 6 | from decimal import Decimal 7 | 8 | class Video: 9 | def __init__(self,filename): 10 | self.filename = filename 11 | self.filesize = self.getFileSize() 12 | example = self.getFrameAt(0) 13 | self.resolution = example.size 14 | self.mode = example.mode 15 | self.duration = self.getVideoDuration() 16 | self.thumbnails = [] 17 | self.thumbsize = self.resolution 18 | self.thumbcount = 0 19 | 20 | def getFileSize(self): 21 | return os.stat(self.filename).st_size / 1048576.0 22 | 23 | def getVideoDuration(self): 24 | p = Popen(["ffmpeg","-i",self.filename],stdout=PIPE, stderr=STDOUT) 25 | pout = p.communicate() 26 | matches = re.search(r"Duration:\s{1}(?P\d+?):(?P\d+?):(?P\d+\.\d+?),", pout[0], re.DOTALL).groupdict() 27 | hours = Decimal(matches['hours']) 28 | minutes = Decimal(matches['minutes']) 29 | seconds = Decimal(matches['seconds']) 30 | duration= 3600*hours + 60*minutes + seconds 31 | return duration 32 | 33 | def getFrameAt(self,seektime): 34 | timestring = self.getTimeString(seektime) 35 | p = Popen(["ffmpeg","-ss",timestring,"-i",self.filename,"-f","image2","-frames:v","1","-c:v","png","-loglevel","8","-"],stdout=PIPE) 36 | pout = p.communicate() 37 | try: 38 | img = Image.open(StringIO.StringIO(pout[0])) 39 | except IOError: 40 | return None 41 | return img 42 | 43 | def makeThumbnails(self,interval): 44 | totalThumbs = self.duration//interval 45 | thumbsList = [] 46 | seektime = 0 47 | for n in range(0,totalThumbs): 48 | seektime += interval 49 | img = self.getFrameAt(seektime) 50 | if img!=None: 51 | thumbsList.append(img) 52 | self.thumbnails = thumbsList 53 | self.thumbcount = len(thumbsList) 54 | return thumbsList 55 | 56 | def shrinkThumbs(self,maxsize): 57 | if self.thumbcount==0: 58 | return 59 | for i in range(0, self.thumbcount): 60 | self.thumbnails[i].thumbnail(maxsize) 61 | self.thumbsize = self.thumbnails[0].size 62 | return self.thumbnails 63 | 64 | def getTimeString(self,seconds): 65 | hours = int(seconds // 3600) 66 | minutes = int((seconds % 3600) // 60) 67 | seconds = int(seconds % 60) 68 | timestring = `hours`+":"+`minutes`+":"+`seconds` 69 | return timestring 70 | 71 | class Sheet: 72 | def __init__(self, video): 73 | fontfile = os.path.join(os.path.dirname(os.path.abspath(__file__)),"Cabin-Regular-TTF.ttf") 74 | self.font = ImageFont.truetype(fontfile, 15) 75 | self.backgroundColour = (0,0,0,0) 76 | self.textColour = (255,255,255,0) 77 | self.headerSize = 100 78 | self.gridColumn = 5 79 | self.maxThumbSize = (220,220) 80 | self.timestamp = True 81 | 82 | self.video = video 83 | 84 | def setProperty(self,prop,value): 85 | if prop == 'font': 86 | self.font = ImageFont.truetype(value[0], value[1]) 87 | elif prop == 'backgroundColour': 88 | self.backgroundColour = value 89 | elif prop == 'textColour': 90 | self.textColour = value 91 | elif prop == 'headerSize': 92 | self.headerSize = value 93 | elif prop == 'gridColumn': 94 | self.gridColumn = value 95 | elif prop == 'maxThumbSize': 96 | self.maxThumbSize = value 97 | elif prop == 'timestamp': 98 | self.timestamp = value 99 | else: 100 | raise Exception('Invalid Sheet property') 101 | 102 | def makeGrid(self): 103 | column = self.gridColumn 104 | row = self.video.thumbcount//column 105 | if (self.video.thumbcount % column) > 0: 106 | row += 1 107 | width = self.video.thumbsize[0] 108 | height = self.video.thumbsize[1] 109 | grid = Image.new(self.video.mode,(width*column,height*row)) 110 | d = ImageDraw.Draw(grid) 111 | seektime = 0 112 | for j in range(0,row): 113 | for i in range(0,column): 114 | if j*column+i >= self.video.thumbcount: 115 | break 116 | grid.paste(self.video.thumbnails[j*column+i],(width*i,height*j)) 117 | if self.timestamp==True: 118 | seektime += self.vid_interval 119 | ts = self.video.getTimeString(seektime) 120 | d.text((width*i,height*j),ts,font=self.font,fill=self.textColour) 121 | self.grid = grid 122 | return grid 123 | 124 | def makeHeader(self): 125 | width = self.video.resolution[0] 126 | height = self.video.resolution[1] 127 | duration = self.video.duration 128 | hours = duration // 3600 129 | minutes = (duration % 3600) // 60 130 | seconds = duration % 60 131 | timestring = ("{:4n}".format(hours))+":"+("{:2n}".format(minutes))+":"+("{:2n}".format(seconds)) 132 | 133 | header = Image.new(self.grid.mode, (self.grid.width,self.headerSize), self.backgroundColour) 134 | d = ImageDraw.Draw(header) 135 | d.text((10,10), "File Name: "+os.path.basename(self.video.filename), font=self.font,fill=self.textColour) 136 | d.text((10,30), "File Size: "+("{:10.6f}".format(self.video.filesize))+" MB", font=self.font,fill=self.textColour) 137 | d.text((10,50), "Resolution: "+`width`+"x"+`height`, font=self.font,fill=self.textColour) 138 | d.text((10,70), "Duration: "+timestring, font=self.font,fill=self.textColour) 139 | self.header = header 140 | return header 141 | 142 | def makeSheetByInterval(self,interval): 143 | self.vid_interval = interval 144 | self.video.makeThumbnails(interval) 145 | self.video.shrinkThumbs(self.maxThumbSize) 146 | self.makeGrid() 147 | self.makeHeader() 148 | self.sheet = Image.new(self.grid.mode,(self.grid.width,self.grid.height+self.header.height)) 149 | self.sheet.paste(self.header,(0,0)) 150 | self.sheet.paste(self.grid,(0,self.header.height)) 151 | return self.sheet 152 | 153 | def makeSheetByNumber(self,numOfThumbs): 154 | interval = (self.video.duration/numOfThumbs) 155 | self.vid_interval = interval 156 | return self.makeSheetByInterval(interval) 157 | -------------------------------------------------------------------------------- /pyVideoSheet/create.py: -------------------------------------------------------------------------------- 1 | from __init__ import Video, Sheet 2 | import argparse 3 | 4 | if __name__ == "__main__": 5 | parser = argparse.ArgumentParser(description='Create thumbnail contact sheet from a video.') 6 | parser.add_argument('filename',help='Input video filename.') 7 | parser.add_argument('--output','-o',default=None, metavar='',help='Specift output video filename.') 8 | parser.add_argument('--interval', '-i', type=int, default=None, metavar='',help='Create thumnnails at fixed interval. Each thumbnail is seconds apart.') 9 | parser.add_argument('--number', '-n', type=int, default=None, metavar='',help='Create total of thumbnails. Each thumbnail is at equidistant apart.') 10 | parser.add_argument('--column','-c',type=int,default=None, metavar='', help='Specify number of column of thumbnail sheet.') 11 | parser.add_argument('--notime', action='count', help='Remove thumbnail timestamp.') 12 | parser.add_argument('--header',type=int,default=None, metavar='', help='Specify height of description header.') 13 | parser.add_argument('--thumbsize','-t', nargs=2,type=int,default=None, metavar=('',''), help='Specify maximum size of a thumbnail. The thumbnails will keep its aspect ratio unchanged.') 14 | parser.add_argument('--textcolour',nargs=4,type=int,default=None, metavar=('','','',''), help='Specify text colour of description. Colour is specify in RGBA format.') 15 | parser.add_argument('--bgcolour',nargs=4,type=int,default=None, metavar=('','','',''), help='Specify background colour of contact sheet. Colour is specify in RGBA format.') 16 | parser.add_argument('--font',nargs=2,default=None, metavar=('',''), help='Specify font of description. Any truetype font are supported.') 17 | parser.add_argument('--preview', action='count', help='Preview the result contact sheet.') 18 | args = parser.parse_args() 19 | 20 | video = Video(args.filename) 21 | sheet = Sheet(video) 22 | 23 | count = 20 24 | mode = 'number' 25 | if args.interval != None: 26 | mode = 'interval' 27 | count = args.interval 28 | if args.number != None: 29 | mode = 'number' 30 | count = args.number 31 | if args.column != None: 32 | c = args.column 33 | if c < 1: 34 | c = 1 35 | sheet.setProperty('gridColumn',c) 36 | if args.header != None: 37 | c = args.header 38 | if c<85: 39 | c=85 40 | sheet.setProperty('headerSize',c) 41 | if args.notime != None: 42 | sheet.setProperty('timestamp',False) 43 | if args.thumbsize != None: 44 | thumbsize = (args.thumbsize[0],args.thumbsize[1]) 45 | sheet.setProperty('maxThumbSize',thumbsize) 46 | if args.textcolour != None: 47 | colour = (args.textcolour[0],args.textcolour[1],args.textcolour[2],args.textcolour[3]) 48 | sheet.setProperty('textColour',colour) 49 | if args.bgcolour != None: 50 | colour = (args.bgcolour[0],args.bgcolour[1],args.bgcolour[2],args.bgcolour[3]) 51 | sheet.setProperty('backgroundColour',colour) 52 | if args.font != None: 53 | font = (args.font[0],int(args.font[1])) 54 | sheet.setProperty('font',font) 55 | 56 | if mode=='number': 57 | sheet.makeSheetByNumber(count) 58 | else: 59 | sheet.makeSheetByInterval(count) 60 | 61 | if args.output != None: 62 | sheet.sheet.save(args.output) 63 | else: 64 | sheet.sheet.save(args.filename[:-3]+'jpg') 65 | 66 | if args.preview != None: 67 | sheet.sheet.show() 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='pyVideoSheet', 7 | version='1.0', 8 | packages = find_packages(), 9 | 10 | package_data = {'':['*.ttf']}, 11 | 12 | description='Python video contact sheet generator', 13 | author='Wattanit Hotrakool', 14 | author_email='wattanit.h@gmail.com', 15 | url='https://github.com/rorasa/pyVideoSheet', 16 | license='MPL 2.0', 17 | ) 18 | --------------------------------------------------------------------------------