Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it more useable out of the box #1

Merged
merged 22 commits into from
May 7, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
79bb2ac
build: rework packaging to PEP517 standard with an src layout and exe…
lrq3000 May 1, 2023
88068cd
build: add a few more trove classifiers
lrq3000 May 1, 2023
12630a9
build: rename gestalt.py to videogestalt.py and fix build and entry p…
lrq3000 May 1, 2023
b9c12ab
feat: can specify custom output path (by default resolves to local fi…
lrq3000 May 2, 2023
b4f00e7
feat: default to input file extension if none is provided with -v
lrq3000 May 2, 2023
6c78a01
docs: how to build
lrq3000 May 2, 2023
c122f6d
docs: how to install in edit mode
lrq3000 May 2, 2023
69ea75d
docs: contributors (necessary for copyright/copyleft)
lrq3000 May 2, 2023
0c5ec81
fix: retrocompatibility with older pip versions
lrq3000 May 7, 2023
d2157f9
feat: append .gif extension only if not already present + update help…
lrq3000 May 7, 2023
9966251
tests: add a simple unit test + simplify example in README + bump v0.2.3
lrq3000 May 7, 2023
321fa72
tests: add continuous integration via GitHub Actions
lrq3000 May 7, 2023
4ab673d
tests: add an empty requirements.txt + add test optional dependencies
lrq3000 May 7, 2023
e8c1a98
fix: setuptools complaining of two independent building config files
lrq3000 May 7, 2023
1abeb14
build: exclude tests from being bundled (add MANIFEST.in)
lrq3000 May 7, 2023
a58e3e2
tests: reduce number of target python versions (tests are slow and en…
lrq3000 May 7, 2023
679905b
tests: ensure line returns are not converted to Windows style when gi…
lrq3000 May 7, 2023
bc833db
tests: remove unrecognized parameter
lrq3000 May 7, 2023
ccf2bc8
tests: oopsie: fix test by adding expected result
lrq3000 May 7, 2023
4bac9ba
tests: remove video tests for now because it is not reproducible + li…
lrq3000 May 7, 2023
916b50c
tests: ensure meta tests are run
lrq3000 May 7, 2023
97c23a7
build: add a continuous deployment workflow through GitHub Actions
lrq3000 May 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 49 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,83 @@
# Video Gestalt

[![gestalt-Vespa-Scooter-Commercial ia mp4][1]][3]
[![gestalt-Vespa-Scooter-Commercial ia mp4][1]][2]

Presents a video in a summary form that shows the entire video at once as an array of moving video thumbnails.

## Description

Video Gestalt presents a condensed video array, showing the entire video at once as moving video thumbnails.

The above is an example of the Video Gestalt for a 50-second commercial for Vesta scooters. (Click the Video Gestalt to see the original video.)

As you can see, it is a looping video with moving thumbnails of the original video. In one second, you can see every frame of the original video at a glance, without any discontinuities as it loops. This is done by arranging that each thumbnail slides over exactly its width in one loop so that the next thumbnail takes over seamlessly.

Hence, the video gestalts can be read in two ways: 1- an overall quick glance shows all the scenes of the entire video, 2- by focusing on one animated thumbnail, we can watch the entire video, by starting in the upper left corner, and following to the right, then descending one block lower and moving from right to left, then descending one block and moving left to right again, etc.

A longer explanation is available in [this blog post](https://eamonn.org/video-gestalt-one-glance-overview-of-a-video).

## Installation

So far this has only been tested on Linux and Chrome OS, but it will likely work on MacOS too, and maybe even on Windows.
So far this has been tested on Linux, Chrome OS and Windows, but it will likely work on MacOS too.

The only file you need from this repo is `gestalt.py`. You can grab this however you want, and make it executable. For example, do the following from the command line:
To install, simply use `pip`:

```bash
wget https://raw.githubusercontent.com/eobrain/videogestalt/main/gestalt.py
chmod +x gestalt.py
pip install --upgrade videogestalt
```

If they are not already installed, you will need to install `python3` and the corresponding Python package manager `pip`.

If they are not already installed, you will need to install `python3` and the corresponding Python package manager `pip` beforehand.

On Linux and friends you might be able to do this like so:
```bash
sudo apt-get install python3 python3-pip
```

You will need to install the `moviepy` Python library:
## Usage

An executable binary `videogestalt` is automatically installed in the local environment.

The following examples assume you cloned the repository to get access to the `example` folder.

To generate a video file:

```bash
videogestalt -i example/test.mp4 -o test-gestalt --video
```

To generate an animated GIF (warning, output can be large):

```bash
pip install moviepy
videogestalt -i example/test.mp4 -o test-gestalt --gif
```

The application can also be used as a Python module:

## Usage
```python
>>> from videogestalt import videogestalt as vg
>>> vg.main('-i example/test.mp4 -o test-gestalt --gif')
```

Put the `gestalt.py` in the same directory as an input video file `test.mp4`.
## Building

Generate a video file:
The module can be built with PEP517 standard tools, such as `pypa/build`:

```bash
./gestalt.py -i test.mp4 -v
python -sBm build .
```

Generate an animated GIF (warning, can be large):
It can also be installed in development/editable mode after cloning this git repository:

```bash
./gestalt.py -i test.mp4 -g
pip install --upgrade -e .
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me (using Python 3.9.2 on Linux) this failed with

ERROR: File "setup.py" not found. Directory cannot be installed in editable mode: /home/eobrain/src/lrq3000
(A "pyproject.toml" file was found, but editable mode currently requires a setup.py based build.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmm I guess your pip version is not updated to the latest, as it should be fixed since pip versions of last year. Nevertheless, I have implemented a workaround now, please retry with my updated PR and let me know if this works :-)

```
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding the commands to run the videogestalt command from the local development version

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the git cloning, this is the only command that should be needed. Now that I fixed the ERROR: File "setup.py" not found, this should work as expected :-)


[1]: https://user-images.githubusercontent.com/179320/226146985-d67db97e-bcd6-4377-a1da-cc6020135d84.gif
[3]: https://ia904607.us.archive.org/11/items/vespa-scooter-commercial/Vespa%20Scooter%20Commercial.mp4
## License

Created by Eamonn O'Brien-Strain.

Licensed under the Mozilla Public License 2.0

[1]: https://github.com/eobrain/videogestalt/master/resources/vespa-commercial-gestalt.gif
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason for pulling the example GIF output into the repo? If not, I suggest leaving it in its original position in GitHib static images.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes usually when an image is meant to be included in the README and not just in the static website, I prefer to include them in the repository to be more future proof, as URLs of static sites may change or be down at any point in the future (it already happened in the past). There is also another pragmatic reason, it's because I don't know where the image is stored currently (I cannot find the source branch from which the image is pushed to GitHub pages?).

[2]: https://ia904607.us.archive.org/11/items/vespa-scooter-commercial/Vespa%20Scooter%20Commercial.mp4

File renamed without changes.
89 changes: 89 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SPDX-License-Identifier: MPL-2.0

[build-system]
# never uppercap requirements unless we have evidence it won't work https://iscinumpy.dev/post/bound-version-constraints/
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project] # beware if using setuptools: setup.py still gets executed, and even if pyproject.toml fields take precedence, if there is any code error in setup.py, building will fail!
name = "videogestalt"
version = "0.2.0" # see PEP 440 https://peps.python.org/pep-0440/#pre-releases and https://packaging.python.org/en/latest/guides/single-sourcing-package-version/
description = "Regular Expression Path Matcher - Easily batch manipulate folders and files trees with regular expression!"
authors = [
{name = "Eamonn O'Brien-Strain"},
]
maintainers = [
{name = "Eamonn O'Brien-Strain"},
]
requires-python = ">=3.7"
license = {text = "Mozilla Public License 2.0"} # { file = "LICENSE" }
keywords = ["video", "gestalt", "movie", "summary", "summarization", "thumbnails"]
classifiers = [
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
'Environment :: Console',
'Operating System :: POSIX :: Linux',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Multimedia :: Video',
'Topic :: Multimedia :: Video :: Display',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Utilities',
]
dependencies = [
"moviepy",
]

[project.urls]
Homepage = "https://github.com/eobrain/videogestalt"
Documentation = "https://github.com/eobrain/videogestalt/blob/master/README.md"
"Source" = "https://github.com/eobrain/videogestalt"
Tracker = "https://github.com/eobrain/videogestalt/issues"
Download = "https://github.com/eobrain/videogestalt/releases"
#Changelog = "https://url/changelog"

[project.optional-dependencies]
testmeta = [ # dependencies to test meta-data
"build",
"twine",
"validate-pyproject",
"rstcheck",
]

[project.readme]
file = "README.md"
content-type = "text/markdown"

[project.scripts]
videogestalt = "videogestalt.videogestalt:main"

#[tool.setuptools]
#package-dir = {"" = "src"}

[tool.setuptools.packages.find]
# IMPORTANT: systematically delete `src/<project.name>.egg-info` folder before rebuilding, otherwise the list of included files will not get updated (it's in `SOURCES.txt` file in this folder)
where = ["src"]
include = ["videogestalt*"]
#namespaces = true # already the default

[tool.setuptools.package-data]
# Check the <mypkg>.egg-info/SOURCES.txt file generated after a `build` or `pip install` to check if the following files are correctly included in the sdist.
# Check also the list of files included by default: https://packaging.python.org/en/latest/guides/using-manifest-in/
"*" = [
"LICENSE*",
"README*",
]
Binary file added resources/vespa-commercial-gestalt.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 70 additions & 20 deletions gestalt.py → src/videogestalt/videogestalt.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#!/usr/bin/env python3


# Author: Eamonn O'Brien-Strain
# Contributors: Stephen Karl Larroque
# License: Mozilla Public License Version 2.0
# Description: Generates overview of video
# Usage: python generate.py -h
# Usage: python videogestalt.py -h

import os
import shlex
import sys
import time

from argparse import ArgumentParser, ArgumentTypeError
from math import sqrt, ceil
from argparse import ArgumentParser
from moviepy.editor import CompositeVideoClip, ImageClip, VideoFileClip, ColorClip
from pathlib import Path

OUTPUT_WIDTH = 1000
MIN_SPEED_PIXELS_PER_FRAME = 2
Expand All @@ -19,12 +25,13 @@
HEIGHT = 1


def main(originalPath, generateGif, generateVideo):
def gen_gestalt(originalPath, outputPath, generateGif, generateVideo):
'''Generate an animated gestalt image or video from a video'''
print("Generating gestalt for %s" % originalPath)
if (generateVideo):
print("Will generating video")
print("Will generate a video")
if (generateGif):
print("Will generating GIF")
print("Will generate a GIF")

original = VideoFileClip(originalPath, audio=False)
minSpeedPixelsPerSec = MIN_SPEED_PIXELS_PER_FRAME*original.fps
Expand Down Expand Up @@ -104,29 +111,72 @@ def right(j):
output = CompositeVideoClip(
thumbs+lefts+rights+[leading, trailing], (extendeWidth, fullHeight))

# Write output
if generateGif:
output.write_gif("gestalt-"+originalPath+".gif", program="ffmpeg")
if generateVideo:
output.write_videofile("gestalt-"+originalPath)


if __name__ == "__main__":
import time

# to a gif file
output.write_gif("%s%s" % (outputPath, ".gif"), program="ffmpeg")
elif generateVideo:
# to a video file

# MoviePy requires a file extension to determine how to encode the output
# Hence, by default, if none is provided, reuse the input file extension
if (outputPath.find('.') < 0): # no extension found
outputPath = "%s%s" % (outputPath, Path(originalPath).suffix) # we append the input path file extension
output.write_videofile(outputPath)

def is_dir_or_file(dirname):
'''Checks if a path is an actual directory that exists or a file'''
if not os.path.isdir(dirname) and not os.path.isfile(dirname):
msg = "{0} is not a directory nor a file".format(dirname)
raise ArgumentTypeError(msg)
else:
return dirname

def fullpath(relpath):
'''Relative path to absolute'''
if (type(relpath) is object or hasattr(relpath, 'read')): # relpath is either an object or file-like, try to get its name
relpath = relpath.name
return os.path.abspath(os.path.expanduser(relpath))

def main(argv=None):
'''Script entry point, can be used in commandline or as a Python module'''
# Allow to be used as a module or in commandline, by storing the commandline arguments in function argument argv if empty
if argv is None: # if argv is empty, fetch from the commandline
argv = sys.argv[1:]
elif isinstance(argv, str): # else if argv is supplied but it's a simple string, we need to parse it to a list of arguments before handing to argparse or any other argument parser
argv = shlex.split(argv) # Parse string just like argv using shlex

# Setup arguments parser
parser = ArgumentParser(
prog='gestalt.py',
description='Generates overview of video',
epilog='(c) Eamonn O\'Brien-Strain')

parser.add_argument('-i', '--input', metavar='something.mp4',
type=is_dir_or_file,
required=True,
help='input video file')
parser.add_argument('-g', '--gif', action='store_true',
parser.add_argument('-o', '--output', metavar='/some/folder/output.(gif|mp4)',
type=str,
required=True,
help='output filepath')

mgroup = parser.add_mutually_exclusive_group(required=True)
mgroup.add_argument('-g', '--gif', action='store_true',
default=False,
help='generate GIF')
parser.add_argument('-v', '--video', action='store_true',
mgroup.add_argument('-v', '--video', action='store_true',
default=False,
help='generate video file')
args = parser.parse_args()
start_time = time.time()
main(args.input, args.gif, args.video)
print("--- %d seconds ---" % (time.time() - start_time))

# Parse arguments (either from commandline or function argument when used as a module)
args = parser.parse_args(argv)

# Generate the gestalt image
start_time = time.time() # note this is not a reliable performance indicator, use time.process_time() instead
gen_gestalt(fullpath(args.input), fullpath(args.output), args.gif, args.video)
print("--- Total time spent: %d seconds ---" % (time.time() - start_time))

# Commandline call
if __name__ == "__main__":
main_entry()