Repository: fourMs/MGT-python (language: python)

Issue:
Included cropvideo in videoreader. Preprocessing looking beautiful

Started documentation latex file

Added a seperate history function

Marcusw

updated jupyter notebook

Test on Windows and Ubuntu
Does it work on Windows and Ubuntu (both local machine and server)?
updated jupyter notebook

Added more descriptions of functions

added some to the documentation

Marcusw

Setup problems
When I am trying to run the setup file, I get the following problem: 

    $ python setup.py 
    Traceback (most recent call last):
      File "setup.py", line 15, in <module>
        with open(os.path.join(_here, 'motionvideo', 'version.py')) as f:
    FileNotFoundError: [Errno 2] No such file or directory: '/home/alexanje/github/MGT-python/motionvideo/version.py'
    
What is wrong?
Notebook for demonstration is under construction

Error in notebook
Testing out the notebook and getting an issues in cell 6: 

      mg = mgmodule.MgObject('dance.avi', starttime = 5, endtime = 15)
      Moviepy - Running:      >>> "+ " ".join(cmd)      Moviepy - Command successful
      ---------------------------------------------------------------------------         NameError                                 Traceback (most recent call last)      <ipython-input-6-b65d648662ba> in <module>      ----> 1 mg = mgmodule.MgObject('dance.avi', starttime = 5, endtime = 15)
      ~/github/MGT-python/mgmodule/__init__.py in __init__(self, filename, method, filtertype, thresh, starttime, endtime, blur, skip, color, contrast, brightness, crop)
          38         self.crop = crop
          39         self.test_input()         ---> 40         self.get_video()
          41 
          42 
      ~/github/MGT-python/mgmodule/__init__.py in get_video(self)
          52     def get_video(self):
          53         """ Creates a video attribute to the Musical Gestures object with the given correct settings. """         ---> 54         self.video, self.length, self.width, self.height, self.fps, self.endtime, self.of = mg_videoreader(self.filename, self.starttime, self.endtime, self.skip, self.contrast, self.brightness, self.crop)
      ~/github/MGT-python/mgmodule/_videoreader.py in mg_videoreader(filename, starttime, endtime, skip, contrast, brightness, crop)
          31         trimvideo = ffmpeg_extract_subclip(filename, starttime, endtime, targetname= of +'_trim' + fex)
          32         of = of + '_trim'         ---> 33         vidcap = cv2.VideoCapture(of+fx)
          34 
          35     # Or just use whole video
      NameError: name 'fx' is not defined

Looks like several issues at once?
Moved history.py

fex instead of .avi

fixed skip bug

Made changes to mgmotion



Discussion / hints:





Ongoing!




the setup.py script is unnecessary to run at this point, but can be useful to have in the future.  

This is on Ubuntu, btw. 
uninstall and re install notebook

>> sudo apt update
>>pip install jupyter
Try again now. I found a typo in _videoreader.py





Fix patch:
diff --git a/.gitignore b/.gitignore
index 55bea8b..6b4ab40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
 __pycache__/
 
-*.avi
 *.mp4
 *.csv
 *.eps
@@ -16,3 +15,4 @@ __pycache__/
 *.txt
 *.tff
 *.tsv
+fix_notebook.py
diff --git a/MotionGesturesToolbox.ipynb b/MotionGesturesToolbox.ipynb
index 2bb211f..c60ad8b 100644
--- a/MotionGesturesToolbox.ipynb
+++ b/MotionGesturesToolbox.ipynb
@@ -1,864 +1,1 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Welcome to the Motion Gestures Toolbox (Python) - Tutorial/Documentation"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## Video visualisation\n",
-    "Videos can be watched as they are, but they can also be used to develop new visualisations to be used for analysis. The aim of creating such alternate displays from video recordings is to uncover features, structures and similarities within the material itself, and in relation to, for example, score material. Three useful visualisation techniques here are motion images, motion history images and motiongrams.\n",
-    "\n",
-    "MGT can generate both dynamic and static visualizations, as well as some quantitative data:\n",
-    "\n",
-    "- dynamic visualisations (video files)\n",
-    "    - motion video\n",
-    "    - motion history video\n",
-    "- static visualisations (images)\n",
-    "    - motion average image\n",
-    "    - motiongrams\n",
-    "    - videograms\n",
-    "- motion data (csv files)\n",
-    "    - quantity of motion\n",
-    "    - centroid of motion\n",
-    "    - area of motion\n",
-    "\n",
-    "In the following we will try this ourselves, and look at the different types."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## Dependencies\n",
-    "\n",
-    "To make sure you have all the necessary dependencies, evaluate the following line in the terminal:\n",
-    "\n",
-    "`pip3 install numpy matplotlib opencv-python moviepy ffmpeg ffmpeg-python scipy`\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## Import\n",
-    "\n",
-    "If you have all the dependencies installed, go ahead and import the `mgmodule`."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import mgmodule"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## The MgObject\n",
-    "\n",
-    "### Simple video import\n",
-    "\n",
-    "Now we create our mg (motion gestures) object. You can simply read a video file this way:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "You can watch your video with calling the `show()` method:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.show() # press q to quit video"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## Preprocessing modules\n",
-    "### Trimming\n",
-    "When creating the object you can also already apply some preprocessing. For example you can trim the duration of the video like this:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# the numbers used for starttime and endtime represent time from the video's timeline in seconds\n",
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15)\n",
-    "mg.show() # view the result, press q to quit video"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "This will create the file *dance_trim.avi* in the same directory."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Skipping\n",
-    "In order to save time, skipping every other frame, or more, in the analysis can give you a faster analysis while still getting an idea of the motion. You can for example set this by adding `skip=2`, to skip two frames before including a frame in the analysis, then skipping two again. "
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=2)\n",
-    "mg.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "This will create the file *dance_trim_skip.avi* in the same directory. Notice how the added suffixes at the end of the file's name can inform you about the processes the material went through. "
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### It's a chain\n",
-    "It is also worth to note that the preprocessing modules work as a chain (- more on that below). In this case that means we first load the video file, then trim its start to 5s and its end to 15s. Then we take the resulting *dance_trim.avi* and discard 2 out of every 3 frames. (Keeping the 1st, skipping 2nd and 3rd, keeping the 4th, skipping 5th and 6th, and so on...) The resulting file of this process is *dance_trim_skip.avi*."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Adjusting contrast and brightness\n",
-    "During preprocessing you can also add (or remove) some contrast and brightness of your video.\n",
-    "\n",
-    "Let's crank up the contrast and brighten up our *dance.avi*!"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, contrast=100, brightness=20)\n",
-    "mg.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Again, the resulting filename *dance_trim_skip_cb.avi* will inform us about the chain of processes *dance.avi* went through."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Cropping\n",
-    "\n",
-    "If the video frame has big areas with no motion occuring, a lot of time could be saved if only the area with motion was used in the analysis. One useful tool developed for the pre-analysis is the crop = 'auto' input, which automatically finds the area with significant motion in the input video. The movement occuring has to be above a low threshold, as to not include irrelevant background motion from shadows, dust etc. Another mode is crop = 'manual', where you can manually mark a rectangle around your area of interest."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "#### Automatic cropping"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, contrast=100, brightness=20, crop='auto')\n",
-    "mg.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "#### Manual cropping"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, contrast=100, brightness=20, crop='manual')\n",
-    "mg.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "The resulting file is now called *dance_trim_skip_cb_crop.avi*."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Summary of preprocessing modules\n",
-    "As we have seen, we can optionally apply four types of preprocessing to the video we load into our MgObject, they are (in order of execution):\n",
-    "\n",
-    "- trim: Trim contents of the video based on `starttime` and `endtime`.\n",
-    "- skip: Skip every n frames, where n is determined by `skip`.\n",
-    "- cb: Adjust contrast and brightness of the video, where `contrast` and `brightness` are the level of adjustment in percentages (meaning `contrast=0` will not apply any change). both values range from `-100` to `100`.\n",
-    "- crop: Crop frames in video. If `crop='auto'` the module will attempt to find the area of motion, if `crop='manual'` we can draw the cropping rectangle over the first frame.\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Keep everything\n",
-    "Notice that although we can optionally apply up to four preprocessing modules to our source video, normally we only keep the final result. If you would like to keep the results of all modules, set `keep_all=True`."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, contrast=100, brightness=20, crop='auto', keep_all=True)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "This will output video files:\n",
-    "- *dance_trim.avi*\n",
-    "- *dance_trim_skip.avi*\n",
-    "- *dance_trim_skip_cb.avi*\n",
-    "- *dance_trim_skip_cb_crop.avi*"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## Processes\n",
-    "\n",
-    "In the following we will take a look at several functions to further process our videos. These include:\n",
-    "- `motion()`: The most frequently used function, generates a *_motion* video, horizontal and vertical motiongrams, and plots about the centroid and quantity of motion found in the video.\n",
-    "- `history()`: Generates a *_history* video by layering the last n frames on the current frame for each frame in the video.\n",
-    "- `motionhistory()`: Generates a *_motionhistory* video, which is equivalent to calling `history()` on a *_motion* video.\n",
-    "- `average()`: Generates an *_average* image of all frames in the video. "
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Motion analysis\n",
-    "\n",
-    "By calling the `motion()` function, we will generate a number of files from the input video, in the same location as the source file.\n",
-    "\n",
-    "These include:\n",
-    "- *<input_filename>_motion.avi*: The motion video that is used as the source for the rest of the analysis.\n",
-    "- *<input_filename>_mgx.png*: A horizontal motiongram.\n",
-    "- *<input_filename>_mgy.png*: A vertical motiongram.\n",
-    "- *<input_filename>_motion_com_qom.png*: An image file with plots of centroid and quantity of motion\n",
-    "\n",
-    "We will examine each of these in a little more detail."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import mgmodule\n",
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=1, contrast=100, brightness=20, crop='auto')\n",
-    "mg.motion()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "ls # take a look at the output files"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "We can now look at the results with using the `key` parameter of `show()`."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.show(key='motion') # show the motion video of the preprocessed input, in this case 'dance_trim_skip_cb_crop_motion.avi'"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.show(key='mgx') # show the horizontal motiongram, here 'dance_trim_skip_cb_crop_mgx.png'"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.show(key='mgy') # show the vertical motiongram"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.show(key='plot') # show the image of the two plots ('Centroid of motion' and 'Quantity of motion') also shown at the end of motion()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Alternatively we can display the images right here in our notebook:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from IPython.display import Image\n",
-    "x = Image('dance_trim_skip_cb_crop_mgx.png')\n",
-    "x"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "y = Image('dance_trim_skip_cb_crop_mgy.png')\n",
-    "y"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "com_qom = Image('dance_trim_skip_cb_crop_motion_com_qom.png')\n",
-    "com_qom"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "#### Filtering\n",
-    "\n",
-    "If you think there is too much noise in the output images or video, you may choose to use some other filter settings. \n",
-    "\n",
-    "Filtertypes availible are: \n",
-    "- `Regular` turns all values below thresh to 0.\n",
-    "- `Binary` turns all values below thresh to 0, above thres to 1.\n",
-    "- `Blob` removes individual pixels with erosion method.\n",
-    "\n",
-    "\n",
-    "Try this:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.motion(filtertype='Blob')\n",
-    "mg.show(key='motion')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### History tracking\n",
-    "As we have seen above, `motion()` is useful if you want to remove the still content of your video, only keeping what is different in subsequent frames. Sometimes it is also useful to visualize changes between frames in a different way: layering the last n frames on top of the current one as a video delay. With `history()` you can achieve this, optionally setting the `history length` to the number of past frames you want to see on the current frame (ie. the length of the delay).\n",
-    "\n",
-    "Try this:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.history()\n",
-    "mg.show(key='history')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "By default `history_length=10`. Let's increase it to 20!"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.history(history_length=20)\n",
-    "mg.show(key='history')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Motion history\n",
-    "\n",
-    "To expressively visualize the trajectory of a moving content in a video, you can apply the history process on a motion video. You can do this in one step with `motionhistory()`."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.motionhistory()\n",
-    "mg.show(key='motionhistory')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Average image\n",
-    "You can also summarize the content of a video by showing the average of all frames in a single image."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.average()\n",
-    "mg.show(key='average')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Embedded in the notebook:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from IPython.display import Image\n",
-    "average = Image('dance_trim_skip_cb_crop_average.png')\n",
-    "average"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "### Optical flow\n",
-    "It is also possible to track the direction certain points - or all points - move in a video, this is called 'optical flow'. It has two types: the *sparse optical flow*, which is for tracking a small (sparse) set of points, visualized with an overlay of dots and lines drawing the trajectory of the chosen points as they move in the video.  "
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.flow.sparse()\n",
-    "mg.show(key='sparse')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Note that sparse optical flow usually works well with slow and continuous movements, where the points to be tracked are not occluded by other objects throughout the course of motion.\n",
-    "Where spare optical flow becomes less reliable, *dense optical flow* often yields more robust results. In dense optical flow the analysis attempts to track the movement of each pixel (or more precisely groups of pixels), colorcoding them with a unique color for each unique direction."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.flow.dense()\n",
-    "mg.show(key='dense')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Sparse optical flow can get confused by too fast movement (ie. too big distance between the locations of a tracked point between two consequtive frames), so it is typically advised not to have a too high `skip` value in the preprocessing stage for it to work properly.\n",
-    "Dense optical flow on the other hand has issues with very slow movement, which sometimes gets below the treshold of what is considered 'a movement' resulting in a blinking video, where the more-or-less idle moments are rendered completely black. If your source video contains such moments, you can try setting `skip_empty=True`, which will discard all the (completely) black frames, eliminating the binking. "
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.flow.dense(skip_empty=True)\n",
-    "mg.show(key='dense')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "## Chaining\n",
-    "\n",
-    "So far our workflow consisted of the following steps:\n",
-    "- 1. Creating an MgObject which loads a video file and optionally applies some preprocessing to it.\n",
-    "- 2. Calling a process on the MgObject.\n",
-    "- 3. Viewing the result.\n",
-    "\n",
-    "Something like this:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3)\n",
-    "mg.motion()\n",
-    "mg.show(key='motion')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "This is convenient if you want to apply several different processes on the same input video."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3)\n",
-    "mg.motion()\n",
-    "mg.history()\n",
-    "mg.motionhistory()\n",
-    "mg.average()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "The Motion Gestures Toolbox also offers an alternative workflow in case you want to apply a proccess on the result of a previous process. Although `show()` is not really a process (ie. it does not yield a file as a result) it can provide a good example of the use of chaining:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# this...\n",
-    "mg.motion().show()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# ...is the equivalent of this!\n",
-    "mg.motion()\n",
-    "mg.show(key='motion')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "It also works with images:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mg.average().show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "But chaining can go further than this. How about reading (and preprocessing) a video, rendering its motion video, the motion history and the average of the motion history, with showing the *_motion_history_average.png* at the end - all as a one-liner?!"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "mgmodule.MgObject('dance.avi', skip=4, crop='auto').motion().history().average().show()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# equivalent without chaining\n",
-    "mg = mgmodule.MgObject('dance.avi', skip=4, crop='auto')\n",
-    "mg.motionhistory()\n",
-    "mh = mgmodule.MgObject('dance_skip_crop_motionhistory.avi')\n",
-    "mh.average()\n",
-    "mh.show(key='average')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Some other examples:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# rendering and viewing the motion video \n",
-    "mgmodule.MgObject('dance.avi', skip=4).motion().show()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# rendering the motion video, the motion history video, and viewing the latter\n",
-    "mgmodule.MgObject('dance.avi', skip=3).motion().history().show()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# rendering the motion video, the motion average image, and viewing the latter\n",
-    "mgmodule.MgObject('dance.avi', skip=15).motion().average().show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "Chaining can also save time (and space) when designing loops for processing a folder of videos. Here is an example:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import os\n",
-    "from mgmodule import MgObject as Mg\n",
-    "\n",
-    "my_videos_folder = 'C:/Users/User/Desktop/test-videos/'\n",
-    "\n",
-    "my_videos = [my_videos_folder + video for video in os.listdir(my_videos_folder) if os.path.splitext(video)[1] in ['.avi', '.mp4', '.mov', '.mkv']]\n",
-    "\n",
-    "for video in my_videos:\n",
-    "    print(f'Processing {video}...')\n",
-    "    Mg(video, skip=10).motion().history().average()"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.7.2-final"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# Welcome to the Motion Gestures Toolbox (Python) - Tutorial/Documentation"}, {"cell_type": "markdown", "metadata": {}, "source": "## Video visualisation\nVideos can be watched as they are, but they can also be used to develop new visualisations to be used for analysis. The aim of creating such alternate displays from video recordings is to uncover features, structures and similarities within the material itself, and in relation to, for example, score material. Three useful visualisation techniques here are motion images, motion history images and motiongrams.\n\nMGT can generate both dynamic and static visualizations, as well as some quantitative data:\n\n- dynamic visualisations (video files)\n    - motion video\n    - motion history video\n- static visualisations (images)\n    - motion average image\n    - motiongrams\n    - videograms\n- motion data (csv files)\n    - quantity of motion\n    - centroid of motion\n    - area of motion\n\nIn the following we will try this ourselves, and look at the different types."}, {"cell_type": "markdown", "metadata": {}, "source": "## Dependencies\n\nTo make sure you have all the necessary dependencies, evaluate the following line in the terminal:\n\n`pip3 install numpy pandas matplotlib opencv-python moviepy ffmpeg ffmpeg-python scipy`\n"}, {"cell_type": "markdown", "metadata": {}, "source": "## Import\n\nIf you have all the dependencies installed, go ahead and import the `mgmodule`."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import mgmodule"}, {"cell_type": "markdown", "metadata": {}, "source": "## The MgObject\n\n### Simple video import\n\nNow we create our mg (motion gestures) object. \n\nYou can simply read a video file from the current directory this way:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi') # from the current directory"}, {"cell_type": "markdown", "metadata": {}, "source": "Relative paths also work. Here is _pianist.avi_ from the __examples__ (sibling) directory:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('./examples/pianist.avi') # from a sibling directory"}, {"cell_type": "markdown", "metadata": {}, "source": "With absolute path:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os\nabs_path = os.path.abspath('dance.avi')\nprint(f\"The absolute path to dance.avi is '{abs_path}'.\")\nmg = mgmodule.MgObject(abs_path) # as absolute path"}, {"cell_type": "markdown", "metadata": {}, "source": "You can watch your video with calling the `show()` method:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.show() # press q to quit video"}, {"cell_type": "markdown", "metadata": {}, "source": "### A brief note on containers and codecs\nWe aim to provide `mgmodule` as a cross-platform, flexible toolbox. Unfortunately different video containers and codecs are not equally well-supported across different containers, API-s, and operating systems. Our current solution to ensure that our toolbox works for you regardless of all that is to use the `mjpeg` codec with the `avi` container. Therefore all imported videos which don't match these are automatically converted (using ffmpeg) as a 0th step, and all subsequent processes are executed on the resulting files."}, {"cell_type": "markdown", "metadata": {}, "source": "## Preprocessing modules\n### Trimming\nWhen creating the object you can also already apply some preprocessing. For example you can trim the duration of the video like this:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# the numbers used for starttime and endtime represent time from the video's timeline in seconds\nmg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15)\nmg.show() # view the result, press q to quit video"}, {"cell_type": "markdown", "metadata": {}, "source": "This will create the file *dance_trim.avi* in the same directory."}, {"cell_type": "markdown", "metadata": {}, "source": "### Skipping\nIn order to save time, skipping every other frame, or more, in the analysis can give you a faster analysis while still getting an idea of the motion. You can for example set this by adding `skip=2`, to skip two frames before including a frame in the analysis, then skipping two again. "}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=2)\nmg.show()"}, {"cell_type": "markdown", "metadata": {}, "source": "This will create the file *dance_trim_skip.avi* in the same directory. Notice how the added suffixes at the end of the file's name can inform you about the processes the material went through. "}, {"cell_type": "markdown", "metadata": {}, "source": "### It's a chain\nIt is also worth to note that the preprocessing modules work as a chain (- more on that below). In this case that means we first load the video file, then trim its start to 5s and its end to 15s. Then we take the resulting *dance_trim.avi* and discard 2 out of every 3 frames. (Keeping the 1st, skipping 2nd and 3rd, keeping the 4th, skipping 5th and 6th, and so on...) The resulting file of this process is *dance_trim_skip.avi*."}, {"cell_type": "markdown", "metadata": {}, "source": "### Rotating\n\nSometimes source videos are recorded with a slightly off horizon, or with the camera mounted sideways, therefore it is desirable to rotate the video by a few (or more) degrees. We can do this by simply specifying the angle we want to rotate with for the `rotate` parameter of our MgObject:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# rotate by 90 degrees\nmg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=90)\nmg.show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# or just a little bit...\nmg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=5.31)\nmg.show()"}, {"cell_type": "markdown", "metadata": {}, "source": "Again, the resulting filename *dance_trim_skip_rot.avi* will inform us about the chain of processes *dance.avi* went through."}, {"cell_type": "markdown", "metadata": {}, "source": "### Adjusting contrast and brightness\nDuring preprocessing you can also add (or remove) some contrast and brightness of your video.\n\nLet's crank up the contrast and brighten up our *dance.avi*!"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=90, contrast=100, brightness=20)\nmg.show()"}, {"cell_type": "markdown", "metadata": {}, "source": "The resulting file is now called *dance_trim_skip_rot_cb.avi*."}, {"cell_type": "markdown", "metadata": {}, "source": "### Cropping\n\nIf the video frame has big areas with no motion occuring, a lot of time could be saved if only the area with motion was used in the analysis. One useful tool developed for the pre-analysis is the crop = 'auto' input, which automatically finds the area with significant motion in the input video. The movement occuring has to be above a low threshold, as to not include irrelevant background motion from shadows, dust etc. Another mode is crop = 'manual', where you can manually mark a rectangle around your area of interest."}, {"cell_type": "markdown", "metadata": {}, "source": "#### Automatic cropping"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=90, contrast=100, brightness=20, crop='auto')\nmg.show()"}, {"cell_type": "markdown", "metadata": {}, "source": "#### Manual cropping"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=90, contrast=100, brightness=20, crop='manual')\nmg.show()"}, {"cell_type": "markdown", "metadata": {}, "source": "The resulting file is now called *dance_trim_skip_rot_cb_crop.avi*."}, {"cell_type": "markdown", "metadata": {}, "source": "### Grayscale mode\n\nSo far all of our work preserved the color space of the source video. At the final preprocessing stage we can also choose to convert the video to grayscale with specifying `color=False` (by default `color=True`). This will not only result in a grayscale version of our source video, but also informs all future processes to work in grayscale mode. The technical benefit of this can be slightly shorter processing times at most processes, since in grayscale mode we process a single color channel per frame (instead of three channels). Try this:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=90, contrast=100, brightness=20, crop='auto', color=False)\nmg.show()"}, {"cell_type": "markdown", "metadata": {}, "source": "The resulting file is now called *dance_trim_skip_rot_cb_crop_gray.avi*."}, {"cell_type": "markdown", "metadata": {}, "source": "### Summary of preprocessing modules\nAs we have seen, we can optionally apply six types of preprocessing to the video we load into our MgObject, they are (in order of execution):\n\n- trim: Trim contents of the video based on `starttime` and `endtime`.\n- skip: Skip every n frames, where n is determined by `skip`.\n- rotate: Rotate the video by an angle determined by `rotate`.\n- cb: Adjust contrast and brightness of the video, where `contrast` and `brightness` are the level of adjustment in percentages (meaning `contrast=0` will not apply any change). both values range from `-100` to `100`.\n- crop: Crop frames in video. If `crop='auto'` the module will attempt to find the area of motion, if `crop='manual'` we can draw the cropping rectangle over the first frame.\n- grayscale: Convert the video to grayscale with specifying `color=False`. This will also cause all further processes called on the MgObject to function in grayscale mode.\n"}, {"cell_type": "markdown", "metadata": {}, "source": "### Keep everything\nNotice that although we can optionally apply up to four preprocessing modules to our source video, normally we only keep the final result. If you would like to keep the results of all modules, set `keep_all=True`."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3, rotate=90, contrast=100, brightness=20, crop='auto', color=False, keep_all=True)"}, {"cell_type": "markdown", "metadata": {}, "source": "This will output six new video files:\n- *dance_trim.avi*\n- *dance_trim_skip.avi*\n- *dance_trim_skip_rot.avi*\n- *dance_trim_skip_rot_cb.avi*\n- *dance_trim_skip_rot_cb_crop.avi*\n- *dance_trim_skip_rot_cb_crop_gray.avi*"}, {"cell_type": "markdown", "metadata": {}, "source": "## Processes\n\nIn the following we will take a look at several functions to further process our videos. These include:\n- `motion()`: The most frequently used function, generates a *_motion* video, horizontal and vertical motiongrams, and plots about the centroid and quantity of motion found in the video.\n- `history()`: Generates a *_history* video by layering the last n frames on the current frame for each frame in the video.\n- `motionhistory()`: Generates a *_motionhistory* video, which is equivalent to calling `history()` on a *_motion* video.\n- `average()`: Generates an *_average* image of all frames in the video.\n- `flow.sparse()`: Generates a *_sparse* optical flow video.\n- `flow.dense()`: Generates a *_dense* optical flow video. "}, {"cell_type": "markdown", "metadata": {}, "source": "### Motion analysis\n\nBy calling the `motion()` function, we will generate a number of files from the input video, in the same location as the source file.\n\nThese include:\n- *<input_filename>_motion.avi*: The motion video that is used as the source for the rest of the analysis.\n- *<input_filename>_mgx.png*: A horizontal motiongram.\n- *<input_filename>_mgy.png*: A vertical motiongram.\n- *<input_filename>_motion_com_qom.png*: An image file with plots of centroid and quantity of motion\n\nWe will examine each of these in a little more detail."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import mgmodule\nmg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=1, contrast=100, brightness=20, crop='auto')\nmg.motion()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "ls # take a look at the output files"}, {"cell_type": "markdown", "metadata": {}, "source": "We can now look at the results with using the `key` parameter of `show()`."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.show(key='motion') # show the motion video of the preprocessed input, in this case 'dance_trim_skip_cb_crop_motion.avi'"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.show(key='mgx') # show the horizontal motiongram, here 'dance_trim_skip_cb_crop_mgx.png'"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.show(key='mgy') # show the vertical motiongram"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.show(key='plot') # show the image of the two plots ('Centroid of motion' and 'Quantity of motion') also shown at the end of motion()"}, {"cell_type": "markdown", "metadata": {}, "source": "Alternatively we can display the images right here in our notebook:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "from IPython.display import Image\nx = Image('dance_trim_skip_cb_crop_mgx.png')\nx"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "y = Image('dance_trim_skip_cb_crop_mgy.png')\ny"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "com_qom = Image('dance_trim_skip_cb_crop_motion_com_qom.png')\ncom_qom"}, {"cell_type": "markdown", "metadata": {}, "source": "You can also configure `motion()` to output only the files you need:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import mgmodule\nmg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=1, contrast=100, brightness=20, crop='auto')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# without plot\nmg.motion(save_plot=False)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# without plot and data\nmg.motion(save_plot=False, save_data=False)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# without plot, data and motiongrams (so only the video)\nmg.motion(save_plot=False, save_data=False, save_motiongrams=False)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# without video, plot and motiongrams (so only the data)\nmg.motion(save_plot=False, save_video=False, save_motiongrams=False)"}, {"cell_type": "markdown", "metadata": {}, "source": "When it comes to the motion data, you can choose from several different formats:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import mgmodule\nmg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=1, contrast=100, brightness=20, crop='auto')"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# save only the data as .csv (default)\nmg.motion(save_plot=False, save_motiongrams=False, save_video=False, data_format=\"csv\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# save only the data as .tsv\nmg.motion(save_plot=False, save_motiongrams=False, save_video=False, data_format=\"tsv\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# save only the data as .txt\nmg.motion(save_plot=False, save_motiongrams=False, save_video=False, data_format=\"txt\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# saving in multiple formats if data_format is a list\nmg.motion(save_plot=False, save_motiongrams=False, save_video=False, data_format=[\"txt\", \"csv\"])"}, {"cell_type": "markdown", "metadata": {}, "source": "#### Filtering types\nIf you think there is too much noise in the output images or video, you may choose to use some other filter settings.\n\nFiltertypes available are:\n\n- `Regular` turns all values below `thresh` to 0.\n- `Binary` turns all values below `thresh` to 0, above `thresh` to 1.\n- `Blob` removes individual pixels with erosion method.\n\nTry this:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.motion(filtertype='Blob')\nmg.show(key='motion')"}, {"cell_type": "markdown", "metadata": {}, "source": "#### Effects of filtering\n\nFinding the right `thresh`old value is crucial for accurate motion extraction. Let's see a few examples.\n\nFirst we import the requirements for the rest of the code."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import mgmodule\nfrom IPython.display import Image"}, {"cell_type": "markdown", "metadata": {}, "source": "Then we import the example video."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=3, endtime=15, skip=0, contrast=100, brightness=20)"}, {"cell_type": "markdown", "metadata": {}, "source": "First we can try to run without any threshold. This will result in a result in which much of the background noise will be visible, including traces of keyframes if the video file has been compressed."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.motion(thresh=0)\nx = Image('dance_trim_cb_mgy.png')\nx"}, {"cell_type": "markdown", "metadata": {}, "source": "Adding just a little bit of thresholding (0.02 here) will drastically improve the final result."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.motion(thresh=0.02)\nx = Image('dance_trim_cb_mgy.png')\nx"}, {"cell_type": "markdown", "metadata": {}, "source": "The standard threshold value (0.1) generally works well for many types of videos."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.motion(thresh=0.1)\nx = Image('dance_trim_cb_mgy.png')\nx"}, {"cell_type": "markdown", "metadata": {}, "source": "A more extreme value (for example 0.5) will remove quite a lot of the content, but may be useful in some cases with very noisy videos."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.motion(thresh=0.5)\nx = Image('dance_trim_cb_mgy.png')\nx"}, {"cell_type": "markdown", "metadata": {}, "source": "As the above examples have shown, choosing the thresholding value is important for the final output result. While it often works to use the default value (0.1), you may improve the result by testing different thresholds."}, {"cell_type": "markdown", "metadata": {}, "source": "### History tracking\nAs we have seen above, `motion()` is useful if you want to remove the still content of your video, only keeping what is different in subsequent frames. Sometimes it is also useful to visualize changes between frames in a different way: layering the last n frames on top of the current one as a video delay. With `history()` you can achieve this, optionally setting the `history length` to the number of past frames you want to see on the current frame (ie. the length of the delay).\n\nTry this:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.history()\nmg.show(key='history')"}, {"cell_type": "markdown", "metadata": {}, "source": "By default `history_length=10`. Let's increase it to 20!"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.history(history_length=20)\nmg.show(key='history')"}, {"cell_type": "markdown", "metadata": {}, "source": "### Motion history\n\nTo expressively visualize the trajectory of a moving content in a video, you can apply the history process on a motion video. You can do this in one step with `motionhistory()`."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.motionhistory()\nmg.show(key='motionhistory')"}, {"cell_type": "markdown", "metadata": {}, "source": "### Average image\nYou can also summarize the content of a video by showing the average of all frames in a single image."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.average()\nmg.show(key='average')"}, {"cell_type": "markdown", "metadata": {}, "source": "Embedded in the notebook:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "from IPython.display import Image\naverage = Image('dance_trim_skip_cb_crop_average.png')\naverage"}, {"cell_type": "markdown", "metadata": {}, "source": "### Optical flow\nIt is also possible to track the direction certain points - or all points - move in a video, this is called 'optical flow'. It has two types: the *sparse optical flow*, which is for tracking a small (sparse) set of points, visualized with an overlay of dots and lines drawing the trajectory of the chosen points as they move in the video.  "}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.flow.sparse()\nmg.show(key='sparse')"}, {"cell_type": "markdown", "metadata": {}, "source": "Note that sparse optical flow usually works well with slow and continuous movements, where the points to be tracked are not occluded by other objects throughout the course of motion.\nWhere spare optical flow becomes less reliable, *dense optical flow* often yields more robust results. In dense optical flow the analysis attempts to track the movement of each pixel (or more precisely groups of pixels), colorcoding them with a unique color for each unique direction."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.flow.dense()\nmg.show(key='dense')"}, {"cell_type": "markdown", "metadata": {}, "source": "Sparse optical flow can get confused by too fast movement (ie. too big distance between the locations of a tracked point between two consequtive frames), so it is typically advised not to have a too high `skip` value in the preprocessing stage for it to work properly.\nDense optical flow on the other hand has issues with very slow movement, which sometimes gets below the treshold of what is considered 'a movement' resulting in a blinking video, where the more-or-less idle moments are rendered completely black. If your source video contains such moments, you can try setting `skip_empty=True`, which will discard all the (completely) black frames, eliminating the binking. "}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.flow.dense(skip_empty=True)\nmg.show(key='dense')"}, {"cell_type": "markdown", "metadata": {}, "source": "## Chaining\n\nSo far our workflow consisted of the following steps:\n- 1. Creating an MgObject which loads a video file and optionally applies some preprocessing to it.\n- 2. Calling a process on the MgObject.\n- 3. Viewing the result.\n\nSomething like this:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3)\nmg.motion()\nmg.show(key='motion')"}, {"cell_type": "markdown", "metadata": {}, "source": "This is convenient if you want to apply several different processes on the same input video."}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg = mgmodule.MgObject('dance.avi', starttime=5, endtime=15, skip=3)\nmg.motion()\nmg.history()\nmg.motionhistory()\nmg.average()"}, {"cell_type": "markdown", "metadata": {}, "source": "The Motion Gestures Toolbox also offers an alternative workflow in case you want to apply a proccess on the result of a previous process. Although `show()` is not really a process (ie. it does not yield a file as a result) it can provide a good example of the use of chaining:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# this...\nmg.motion().show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# ...is the equivalent of this!\nmg.motion()\nmg.show(key='motion')"}, {"cell_type": "markdown", "metadata": {}, "source": "It also works with images:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mg.average().show()"}, {"cell_type": "markdown", "metadata": {}, "source": "But chaining can go further than this. How about reading (and preprocessing) a video, rendering its motion video, the motion history and the average of the motion history, with showing the *_motion_history_average.png* at the end - all as a one-liner?!"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "mgmodule.MgObject('dance.avi', skip=4, crop='auto').motion().history().average().show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# equivalent without chaining\nmg = mgmodule.MgObject('dance.avi', skip=4, crop='auto')\nmg.motionhistory()\nmh = mgmodule.MgObject('dance_skip_crop_motionhistory.avi')\nmh.average()\nmh.show(key='average')"}, {"cell_type": "markdown", "metadata": {}, "source": "Some other examples:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# rendering and viewing the motion video \nmgmodule.MgObject('dance.avi', skip=4).motion().show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# rendering the motion video, the motion history video, and viewing the latter\nmgmodule.MgObject('dance.avi', skip=3).motion().history().show()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# rendering the motion video, the motion average image, and viewing the latter\nmgmodule.MgObject('dance.avi', skip=15).motion().average().show()"}, {"cell_type": "markdown", "metadata": {}, "source": "Chaining can also save time (and space) when designing loops for processing a folder of videos. Here is an example:"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os\nfrom mgmodule import MgObject as Mg\n\nmy_videos_folder = 'C:/Users/User/Desktop/test-videos/'\n\nmy_videos = [my_videos_folder + video for video in os.listdir(my_videos_folder) if os.path.splitext(video)[1] in ['.avi', '.mp4', '.mov', '.mkv']]\n\nfor video in my_videos:\n    print(f'Processing {video}...')\n    Mg(video, skip=10).motion().history().average()"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ""}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.2-final"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/dance.avi b/dance.avi
similarity index 100%
rename from examples/dance.avi
rename to dance.avi
diff --git a/mgmodule/__init__.py b/mgmodule/__init__.py
index e4dc524..a48b5f6 100755
--- a/mgmodule/__init__.py
+++ b/mgmodule/__init__.py
@@ -1,31 +1,83 @@
 import os
 from ._input_test import mg_input_test
 from ._videoreader import mg_videoreader
-from ._constrainNumber import constrainNumber
 from ._flow import Flow
 
 
 class MgObject:
     """ 
-    Initializes Musical Gestures data structure from a given parameter video file.
-
-    Parameters:
-    -----------
-    - filename (str): Name of input parameter video file.
-    - method (str): Currently 'Diff' is the only implemented method.
-    - filtertype (str): 'Regular', 'Binary', 'Blob' (see function filterframe).
-    - thresh (float): A number in [0,1]. Eliminates pixel values less than given threshold.
-    - starttime (float): Cut the video from this start time (min) to analyze what is relevant.
-    - endtime (float): Cut the video at this end time (min) to analyze what is relevant.
-    - blur (str): 'Average' to apply a blurring filter, 'None' otherwise.
-    - skip (int): When proceeding to analyze next frame of video, this many frames are skipped.
-    - color (bool): True does the analysis in RGB, False in grayscale.
-    - contrast (float): Apply +/- 100 contrast to video.
-    - brightness (float): Apply +/- 100 brightness to video.
-    - crop (str): 'none', 'manual', 'auto' to select cropping of relevant video frame size.
+    Initializes Musical Gestures data structure from a video file.
+
+    Attributes
+    ----------
+    - filename : str
+
+        Path to the video file.
+    - filtertype : {'Regular', 'Binary', 'Blob'}, optional
+
+        The `filtertype` parameter for the `motion()` method.
+        `Regular` turns all values below `thresh` to 0.
+        `Binary` turns all values below `thresh` to 0, above `thresh` to 1.
+        `Blob` removes individual pixels with erosion method.
+
+    - thresh : float, optional
+
+        The `thresh` parameter for the `motion()` method.
+        A number in the range of 0 to 1. Default is 0.05.
+        Eliminates pixel values less than given threshold.
+    - starttime : int or float, optional
+
+        Trims the video from this start time (s).
+    - endtime : int or float, optional
+
+        Trims the video until this end time (s).
+    - blur : {'None', 'Average'}, optional
+
+        The `blur` parameter for the `motion()` method.
+        `Average` to apply a 10px * 10px blurring filter, `None` otherwise.
+    - skip : int, optional
+
+        Time-shrinks the video by skipping (discarding) every n frames determined by `skip`.
+    - rotate : int or float, optional
+
+        Rotates the video by a `rotate` degrees.
+    - color : bool, optional
+
+        Default is `True`. If `False`, converts the video to grayscale and sets every method in grayscale mode.
+    - contrast : int or float, optional
+
+        Applies +/- 100 contrast to video.
+    - brightness : int or float, optional
+
+        Applies +/- 100 brightness to video.
+    - crop : {'none', 'manual', 'auto'}, optional
+
+        If `manual`, opens a window displaying the first frame of the input video file,
+        where the user can draw a rectangle to which cropping is applied.
+        If `auto` the cropping function attempts to determine the area of significant motion 
+        and applies the cropping to that area.
+
+    - keep_all : bool, optional
+
+        Default is `False`. If `True`, preserves an output video file after each used preprocessing stage.
     """
 
-    def __init__(self, filename, method='Diff', filtertype='Regular', thresh=0.05, starttime=0, endtime=0, blur='None', skip=0, color=True, contrast=0, brightness=0, crop='None', keep_all=False):
+    def __init__(
+            self,
+            filename,
+            filtertype='Regular',
+            thresh=0.05,
+            starttime=0,
+            endtime=0,
+            blur='None',
+            skip=0,
+            rotate=0,
+            color=True,
+            contrast=0,
+            brightness=0,
+            crop='None',
+            keep_all=False,
+            returned_by_process=False):
 
         self.filename = filename
         # name of file without extension (only-filename)
@@ -33,7 +85,6 @@ def __init__(self, filename, method='Diff', filtertype='Regular', thresh=0.05, s
         # file extension
         self.fex = os.path.splitext(self.filename)[1]
         self.color = color
-        self.method = method
         self.starttime = starttime
         self.endtime = endtime
         self.skip = skip
@@ -43,10 +94,12 @@ def __init__(self, filename, method='Diff', filtertype='Regular', thresh=0.05, s
         self.contrast = contrast
         self.brightness = brightness
         self.crop = crop
+        self.rotate = rotate
         self.keep_all = keep_all
+        self.returned_by_process = returned_by_process
         self.test_input()
         self.get_video()
-        self.flow = Flow(self.filename)
+        self.flow = Flow(self.filename, self.color)
 
     from ._motionvideo import mg_motionvideo as motion
     from ._motionvideo import plot_motion_metrics
@@ -58,13 +111,13 @@ def __init__(self, filename, method='Diff', filtertype='Regular', thresh=0.05, s
 
     def test_input(self):
         """ Gives feedback to user if initialization from input went wrong. """
-        mg_input_test(self.filename, self.method, self.filtertype,
+        mg_input_test(self.filename, self.filtertype,
                       self.thresh, self.starttime, self.endtime, self.blur, self.skip)
 
     def get_video(self):
         """ Creates a video attribute to the Musical Gestures object with the given correct settings. """
-        self.video, self.length, self.width, self.height, self.fps, self.endtime, self.of, self.fex = mg_videoreader(
-            self.filename, self.starttime, self.endtime, self.skip, self.contrast, self.brightness, self.crop, keep_all=self.keep_all)
+        self.length, self.width, self.height, self.fps, self.endtime, self.of, self.fex = mg_videoreader(
+            self.filename, self.starttime, self.endtime, self.skip, self.rotate, self.contrast, self.brightness, self.crop, self.color, self.returned_by_process, keep_all=self.keep_all)
 
         # update filename after the processes
         self.filename = self.of + self.fex
diff --git a/mgmodule/_average.py b/mgmodule/_average.py
index f0f26d7..cb68d31 100755
--- a/mgmodule/_average.py
+++ b/mgmodule/_average.py
@@ -6,19 +6,26 @@
 
 def mg_average_image(self, filename='', normalize=True):
     """
-    Post-processing tool. Finds and saves an average image of entire video.
-
-        THIS DOC STRING IS COMPLETELY WRONG. WILL FIX IT LATER.
+    Finds and saves an average image of an input video file.
 
     Parameters
     ----------
-    - enhance (float): Takes values between '0' and '1'. Where '0' is no enhancement and '1' scales the pixel 
-            values such that the brightest pixel gets the value 255. 
+    - filename : str, optional
+
+        Path to the input video file. If not specified the video file pointed to by the MgObject is used.
+    - normalize : bool, optional
+
+        Default is `True`. If `True`, normalizes pixel values in the output image.
 
-    Usage
-    -----
-    from _motionaverage import motionaverage
-    motionaverage('filename.avi', enhance = 0.5)
+    Outputs
+    -------
+    - `filename`_average.png
+
+    Returns
+    -------
+    - MgImage
+
+        A new MgImage pointing to the output '_average' image file.
     """
 
     if filename == '':
@@ -28,23 +35,28 @@ def mg_average_image(self, filename='', normalize=True):
     video = cv2.VideoCapture(filename)
     ret, frame = video.read()
     length = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
+    if self.color == False:
+        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
     average = frame.astype(np.float)/length
     ii = 0
     while(video.isOpened()):
         ret, frame = video.read()
         if ret == True:
+            if self.color == False:
+                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
             frame = np.array(frame)
             frame = frame.astype(np.float)
             average += frame/length
         else:
-            #print('Rendering average image 100%%')
             mg_progressbar(
                 length, length, 'Rendering average image:', 'Complete')
             break
         ii += 1
-        #print('Rendering average image %s%%' %(int(ii/(length-1)*100)), end='\r')
         mg_progressbar(ii, length, 'Rendering average image:', 'Complete')
 
+    if self.color == False:
+        average = cv2.cvtColor(average.astype(np.uint8), cv2.COLOR_GRAY2BGR)
+
     if normalize:
         average = average/np.max(average)*255
         average = average.astype(np.uint8)
diff --git a/mgmodule/_centroid.py b/mgmodule/_centroid.py
index 79b28d7..b11dfcb 100755
--- a/mgmodule/_centroid.py
+++ b/mgmodule/_centroid.py
@@ -1,16 +1,31 @@
 import cv2
 import numpy as np
+
+
 def centroid(image, width, height):
     """
-    Computes the centroid of an image/frame.
-    
+    Computes the centroid of an image or frame.
+
     Parameters
-    - image (uint8)
-    - width/height of image
-    
-    Returns:
-    - Centroid of motion: Where was the maximum change in pixel value
-    - Quantity of motion: How large was the change in pixel value
+    ----------
+    - image : np.array(uint8)
+
+        The input image matrix for the centroid estimation function. 
+    - width : int
+
+        The pixel width of the input video capture. 
+    - height : int
+
+        The pixel height of the input video capture. 
+
+    Returns
+    -------
+    - np.array(2)
+
+        X and Y coordinates of the centroid of motion.
+    - int
+
+        Quantity of motion: How large the change was in pixels.
     """
 
     image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
@@ -18,18 +33,19 @@ def centroid(image, width, height):
     x = np.arange(width)
     y = np.arange(height)
     qom = cv2.sumElems(image)[0]
-    mx = np.mean(image,axis=0)
-    my = np.mean(image,axis=1)
+    mx = np.mean(image, axis=0)
+    my = np.mean(image, axis=1)
 
     if np.sum(mx) != 0 and np.sum(my) != 0:
-        comx = x.reshape(1,width)@mx.reshape(width,1)/np.sum(mx)
-        comy = y.reshape(1,height)@my.reshape(height,1)/np.sum(my)
+        comx = x.reshape(1, width)@mx.reshape(width, 1)/np.sum(mx)
+        comy = y.reshape(1, height)@my.reshape(height, 1)/np.sum(my)
     else:
         comx = 0
         comy = 0
-  
+
     com = np.zeros(2)
-    com[0]=comx
-    com[1]=height-comy # The y-axis is flipped to fit a "normal" coordinate system
+    com[0] = comx
+    # The y-axis is flipped to fit a "normal" coordinate system
+    com[1] = height-comy
 
-    return com, qom
+    return com, int(qom)
diff --git a/mgmodule/_constrainNumber.py b/mgmodule/_constrainNumber.py
deleted file mode 100755
index 87f701c..0000000
--- a/mgmodule/_constrainNumber.py
+++ /dev/null
@@ -1,13 +0,0 @@
-def constrainNumber(n, minn, maxn):
-    """
-    Constraining number to lie between minn and maxn
-    
-    Parameters:
-    - n (number)
-    - minn (lower limit n can be)
-    - maxn (lower limit n can be)
-    
-    return:
-    Constrained number
-    """
-    return max(min(maxn, n), minn)
diff --git a/mgmodule/_cropvideo.py b/mgmodule/_cropvideo.py
index 2e72f9b..8606b3a 100755
--- a/mgmodule/_cropvideo.py
+++ b/mgmodule/_cropvideo.py
@@ -6,194 +6,222 @@
 from ._filter import filter_frame
 
 
-def mg_cropvideo(fps,width,height, length, of, fex, crop_movement = 'Auto', motion_box_thresh = 0.1, motion_box_margin = 1):
-	"""
-		Crops the video.
-
-		Arguments
-		---------
-		- crop_movement : {'Auto','Manual'}
-			'Auto' finds the bounding box that contains the total motion in the video.
-			Motion threshold is given by motion_box_thresh. 'Manual' opens up a simple 
-			GUI that is used to crop the video manually by looking at the first frame.
-		- motion_box_thresh : float 
-			Only meaningful is crop_movement = 'Auto'. Takes floats between 0 and 1,
-			where 0 includes all the motion and 1 includes none.
-		- motion_box_margin : int 
-			Only meaningful is crop_movement = 'Auto'. Add margin to the bounding box.
-	
-		Returns
-		-------
-		- None
-	"""
-
-	global frame_mask,drawing,g_val,x_start,x_stop,y_start,y_stop 
-	x_start,y_start = -1,-1
-	x_stop,y_stop = -1,-1
-
-	drawing = False
-
-
-	vid2crop = cv2.VideoCapture(of + fex)
-	vid2findbox = cv2.VideoCapture(of + fex)
-
-	ret, frame = vid2crop.read()
-
-	if crop_movement.lower() == 'manual':
-		frame_mask = np.zeros(frame.shape)
-		name_str = 'Draw rectangle and press "C" to crop'
-		cv2.namedWindow(name_str)
-		cv2.setMouseCallback(name_str,draw_rectangle,param = frame)
-		g_val = 220
-		while(1):
-			cv2.imshow(name_str,frame*(frame_mask!=g_val)+frame_mask.astype(np.uint8))
-			k = cv2.waitKey(1) & 0xFF
-			if k == ord('c') or k == ord('C'):
-				break
-		cv2.destroyAllWindows()
-
-	
-		if x_stop<x_start:
-			temp=x_start
-			x_start=x_stop
-			x_stop = temp
-		if y_stop<y_start:
-			temp=y_start
-			y_start=y_stop
-			y_stop = temp
-	elif crop_movement.lower() == 'auto':
-		[x_start,x_stop,y_start,y_stop] = find_total_motion_box(vid2findbox,width,height,length,motion_box_thresh,motion_box_margin)
-	fourcc = cv2.VideoWriter_fourcc(*'MJPG')
-	out = cv2.VideoWriter(of + '_crop' + fex,fourcc, fps, (int(x_stop-x_start),(int(y_stop-y_start))))
-	ii = 0 
-	while (vid2crop.isOpened()):
-		if ret:
-			frame_temp = frame[y_start:y_stop,x_start:x_stop,:]
-			out.write(frame_temp)
-			ret, frame = vid2crop.read()
-		else:
-			#print('Cropping video 100%')
-			mg_progressbar(length, length, 'Rendering cropped video:', 'Complete')
-			break
-			
-		ii+=1
-		#print('Cropping video %s%%' %(int(ii/(length-1)*100)), end='\r')
-		mg_progressbar(ii, length+1, 'Rendering cropped video:', 'Complete')
-
-	vid2crop.release()
-	out.release()
-	cv2.destroyAllWindows()
-
-	vidcap = cv2.VideoCapture(of + '_crop' + fex)
-	width = int(vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
-	height = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
-	return vidcap,width,height
-
-def draw_rectangle(event,x,y,flags,param):
-	global x_start,y_start,x_stop,y_stop,drawing,frame_mask
-	if event == cv2.EVENT_LBUTTONDOWN:
-		frame_mask = np.zeros(param.shape)
-		drawing = True
-		x_start,y_start = x,y
-
-	elif event == cv2.EVENT_MOUSEMOVE:
-		if drawing == True:
-			frame_mask = np.zeros(param.shape)
-			cv2.rectangle(frame_mask,(x_start,y_start),(x,y),(g_val,g_val,g_val),1)
-
-
-	elif event == cv2.EVENT_LBUTTONUP:
-		drawing = False
-		x_stop,y_stop = x,y
-		cv2.rectangle(frame_mask,(x_start,y_start),(x,y),(g_val,g_val,g_val),1)
+def mg_cropvideo(
+        fps,
+        width,
+        height,
+        length,
+        of,
+        fex,
+        crop_movement='Auto',
+        motion_box_thresh=0.1,
+        motion_box_margin=1):
+    """
+    Crops the video.
+
+    Parameters
+    ----------
+    - fps : int
+
+        The FPS (frames per second) of the input video capture.
+    - width : int
+
+        The pixel width of the input video capture. 
+    - height : int
+
+        The pixel height of the input video capture. 
+    - length : int
+
+        The number of frames in the input video capture.
+    - of : str
+
+        'Only filename' without extension (but with path to the file).
+    - fex : str
+
+        File extension.
+    - crop_movement : {'Auto','Manual'}, optional
+
+        'Auto' finds the bounding box that contains the total motion in the video.
+        Motion threshold is given by motion_box_thresh. 'Manual' opens up a simple 
+        GUI that is used to crop the video manually by looking at the first frame.
+    - motion_box_thresh : float, optional
+
+        Only meaningful if `crop_movement='Auto'`. Takes floats between 0 and 1,
+        where 0 includes all the motion and 1 includes none.
+    - motion_box_margin : int, optional
+
+        Only meaningful if `crop_movement='Auto'`. Adds margin to the bounding box.
+    """
+
+    global frame_mask, drawing, g_val, x_start, x_stop, y_start, y_stop
+    x_start, y_start = -1, -1
+    x_stop, y_stop = -1, -1
+
+    drawing = False
+
+    vid2crop = cv2.VideoCapture(of + fex)
+    vid2findbox = cv2.VideoCapture(of + fex)
+
+    ret, frame = vid2crop.read()
+
+    if crop_movement.lower() == 'manual':
+        frame_mask = np.zeros(frame.shape)
+        name_str = 'Draw rectangle and press "C" to crop'
+        cv2.namedWindow(name_str)
+        cv2.setMouseCallback(name_str, draw_rectangle, param=frame)
+        g_val = 220
+        while(1):
+            cv2.imshow(name_str, frame*(frame_mask != g_val) +
+                       frame_mask.astype(np.uint8))
+            k = cv2.waitKey(1) & 0xFF
+            if k == ord('c') or k == ord('C'):
+                break
+        cv2.destroyAllWindows()
+
+        if x_stop < x_start:
+            temp = x_start
+            x_start = x_stop
+            x_stop = temp
+        if y_stop < y_start:
+            temp = y_start
+            y_start = y_stop
+            y_stop = temp
+    elif crop_movement.lower() == 'auto':
+        [x_start, x_stop, y_start, y_stop] = find_total_motion_box(
+            vid2findbox, width, height, length, motion_box_thresh, motion_box_margin)
+    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
+    out = cv2.VideoWriter(of + '_crop' + fex, fourcc, fps,
+                          (int(x_stop-x_start), (int(y_stop-y_start))))
+    ii = 0
+    while (vid2crop.isOpened()):
+        if ret:
+            frame_temp = frame[y_start:y_stop, x_start:x_stop, :]
+            out.write(frame_temp)
+            ret, frame = vid2crop.read()
+        else:
+            mg_progressbar(
+                length, length, 'Rendering cropped video:', 'Complete')
+            break
+
+        ii += 1
+        mg_progressbar(ii, length+1, 'Rendering cropped video:', 'Complete')
+
+    vid2crop.release()
+    out.release()
+    cv2.destroyAllWindows()
+
+    vidcap = cv2.VideoCapture(of + '_crop' + fex)
+    width = int(vidcap.get(cv2.CAP_PROP_FRAME_WIDTH))
+    height = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+    return vidcap, width, height
+
+
+def draw_rectangle(event, x, y, flags, param):
+    global x_start, y_start, x_stop, y_stop, drawing, frame_mask
+    if event == cv2.EVENT_LBUTTONDOWN:
+        frame_mask = np.zeros(param.shape)
+        drawing = True
+        x_start, y_start = x, y
+
+    elif event == cv2.EVENT_MOUSEMOVE:
+        if drawing == True:
+            frame_mask = np.zeros(param.shape)
+            cv2.rectangle(frame_mask, (x_start, y_start),
+                          (x, y), (g_val, g_val, g_val), 1)
+
+    elif event == cv2.EVENT_LBUTTONUP:
+        drawing = False
+        x_stop, y_stop = x, y
+        cv2.rectangle(frame_mask, (x_start, y_start),
+                      (x, y), (g_val, g_val, g_val), 1)
 
 
 def find_motion_box(grayimage, width, height, motion_box_margin):
-	prev_Start = width
-	prev_Stop = 0 
-
-	the_box = np.zeros([height,width])
-	#----Finding left and right edges
-	le = int(width/2)
-	re = int(width/2)
-	for i in range(height):
-		row=grayimage[i,:]
-		inds = np.where(row>0)[0]
-		if len(inds)>0:
-			Start = inds[0]
-			if Start<prev_Start:
-				le = Start
-				prev_Start = Start
-			if len(inds)>1:
-				Stop = inds[-1]
-				if Stop>prev_Stop:
-					re = Stop
-					prev_Stop = Stop
-
-	# ---- Finding top and bottom edges
-	prev_Start = height
-	prev_Stop = 0 
-	te = int(height/2)
-	be = int(height/2)
-	for j in range(width):
-		col=grayimage[:,j]
-		inds = np.where(col>0)[0]
-		if len(inds)>0:
-			Start = inds[0]
-			if Start<prev_Start:
-				te = Start
-				prev_Start = Start
-			if len(inds)>1:
-				Stop = inds[-1]
-				if Stop>prev_Stop:
-					be = Stop
-					prev_Stop = Stop
-
-	margin = motion_box_margin
-
-	x_start = np.clip(le-margin,0,width-1)
-	x_stop = np.clip(re+margin,0,width-1)
-	y_start = np.clip(te-margin,0,height-1)
-	y_stop = np.clip(be+margin,0,height-1)
-
-	the_box[np.clip(te-margin,0,height-1),np.clip(le-margin,0,width-1):np.clip(re+margin,0,width-1)]=1
-	the_box[np.clip(te-margin,0,height-1):np.clip(be+margin,0,height-1),np.clip(le-margin,0,width-1)]=1
-	the_box[np.clip(be+margin,0,height-1),np.clip(le-margin,0,width-1):np.clip(re+margin,0,width-1)]=1
-	the_box[np.clip(te-margin,0,height-1):np.clip(be+margin,0,height-1),np.clip(re+margin,0,width-1)]=1
-
-	return the_box,x_start,x_stop,y_start,y_stop
-
-def find_total_motion_box(vid2findbox,width,height,length,motion_box_thresh,motion_box_margin):
-	total_box = np.zeros([height,width])
-	ret, frame = vid2findbox.read()
-	frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
-	ii=0
-	while(vid2findbox.isOpened()):
-		prev_frame = frame.astype(np.int32)
-		ret, frame = vid2findbox.read()
-		if ret==True:
-			ii+=1
-			#print('Finding area of motion %s%%' %(int(ii/(length-1)*100)), end='\r')
-			mg_progressbar(ii, length, 'Finding area of motion:', 'Complete')
-			frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
-			frame = frame.astype(np.int32)
-
-			motion_frame = (np.abs(frame-prev_frame)).astype(np.uint8)
-			motion_frame = filter_frame(motion_frame,'Regular',thresh = motion_box_thresh, kernel_size=5)
-
-			[the_box,x_start,x_stop,y_start,y_stop]=find_motion_box(motion_frame,width,height,motion_box_margin)
-			total_box = total_box*(the_box==0)+the_box
-		else:
-			[total_motion_box,x_start,x_stop,y_start,y_stop] = find_motion_box(total_box,width,height,motion_box_margin)
-			#print('Finding area of motion 100%')
-			mg_progressbar(length, length, 'Finding area of motion:', 'Complete')
-			break
-
-	return x_start,x_stop,y_start,y_stop
-
-
-
-
-
-
-
+    prev_Start = width
+    prev_Stop = 0
+
+    the_box = np.zeros([height, width])
+    # ----Finding left and right edges
+    le = int(width/2)
+    re = int(width/2)
+    for i in range(height):
+        row = grayimage[i, :]
+        inds = np.where(row > 0)[0]
+        if len(inds) > 0:
+            Start = inds[0]
+            if Start < prev_Start:
+                le = Start
+                prev_Start = Start
+            if len(inds) > 1:
+                Stop = inds[-1]
+                if Stop > prev_Stop:
+                    re = Stop
+                    prev_Stop = Stop
+
+    # ---- Finding top and bottom edges
+    prev_Start = height
+    prev_Stop = 0
+    te = int(height/2)
+    be = int(height/2)
+    for j in range(width):
+        col = grayimage[:, j]
+        inds = np.where(col > 0)[0]
+        if len(inds) > 0:
+            Start = inds[0]
+            if Start < prev_Start:
+                te = Start
+                prev_Start = Start
+            if len(inds) > 1:
+                Stop = inds[-1]
+                if Stop > prev_Stop:
+                    be = Stop
+                    prev_Stop = Stop
+
+    margin = motion_box_margin
+
+    x_start = np.clip(le-margin, 0, width-1)
+    x_stop = np.clip(re+margin, 0, width-1)
+    y_start = np.clip(te-margin, 0, height-1)
+    y_stop = np.clip(be+margin, 0, height-1)
+
+    the_box[np.clip(te-margin, 0, height-1), np.clip(le-margin,
+                                                     0, width-1):np.clip(re+margin, 0, width-1)] = 1
+    the_box[np.clip(te-margin, 0, height-1):np.clip(be+margin,
+                                                    0, height-1), np.clip(le-margin, 0, width-1)] = 1
+    the_box[np.clip(be+margin, 0, height-1), np.clip(le-margin,
+                                                     0, width-1):np.clip(re+margin, 0, width-1)] = 1
+    the_box[np.clip(te-margin, 0, height-1):np.clip(be+margin,
+                                                    0, height-1), np.clip(re+margin, 0, width-1)] = 1
+
+    return the_box, x_start, x_stop, y_start, y_stop
+
+
+def find_total_motion_box(vid2findbox, width, height, length, motion_box_thresh, motion_box_margin):
+    total_box = np.zeros([height, width])
+    ret, frame = vid2findbox.read()
+    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+    ii = 0
+    while(vid2findbox.isOpened()):
+        prev_frame = frame.astype(np.int32)
+        ret, frame = vid2findbox.read()
+        if ret == True:
+            ii += 1
+            mg_progressbar(ii, length, 'Finding area of motion:', 'Complete')
+            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+            frame = frame.astype(np.int32)
+
+            motion_frame = (np.abs(frame-prev_frame)).astype(np.uint8)
+            motion_frame = filter_frame(
+                motion_frame, 'Regular', thresh=motion_box_thresh, kernel_size=5)
+
+            [the_box, x_start, x_stop, y_start, y_stop] = find_motion_box(
+                motion_frame, width, height, motion_box_margin)
+            total_box = total_box*(the_box == 0)+the_box
+        else:
+            [total_motion_box, x_start, x_stop, y_start, y_stop] = find_motion_box(
+                total_box, width, height, motion_box_margin)
+            mg_progressbar(
+                length, length, 'Finding area of motion:', 'Complete')
+            break
+
+    return x_start, x_stop, y_start, y_stop
diff --git a/mgmodule/_filter.py b/mgmodule/_filter.py
index b402a8b..dd37e56 100755
--- a/mgmodule/_filter.py
+++ b/mgmodule/_filter.py
@@ -5,29 +5,39 @@
 
 def filter_frame(motion_frame, filtertype, thresh, kernel_size):
     """
-    Apply a filter to a picture/videoframe.
+    Applies a filter to an image or videoframe.
 
     Parameters
     ----------
-    - motion_frame (array(uint8)): input motion image
-    - filtertype (str):
-                    ’Regular’, turns all values below thresh to 0,
-                    ’Binary’ truns all values below thres to 0, above thres to 1,
-                    ’Blob’ removes individual pixels with erosion method.
-    - thresh (float): for ’Regular’ and ’Binary’ option, thresh is a value of threshold [0,1];
-    - kernel_size(int): Size of structuring element
+    - motion_frame : np.array(uint8) 
+
+        Input motion image.
+    - filtertype : {'Regular', 'Binary', 'Blob'}
+
+        `Regular` turns all values below `thresh` to 0.
+        `Binary` turns all values below `thresh` to 0, above `thresh` to 1.
+        `Blob` removes individual pixels with erosion method.
+    - thresh : float
+
+        A number in the range of 0 to 1.
+        Eliminates pixel values less than given threshold.
+    - kernel_size : int
+
+        Size of structuring element.
 
     Returns
     -------
-    - filtered frame (array(uint8))
+    - np.array(uint8)
+
+        Filtered frame.
     """
-    if filtertype == 'Regular':
+    if filtertype.lower() == 'regular':
         motion_frame = (motion_frame > thresh*255)*motion_frame
         motion_frame = medfilt2d(motion_frame, kernel_size)
-    elif filtertype == 'Binary':
+    elif filtertype.lower() == 'binary':
         motion_frame = (motion_frame > thresh*255)*255
         motion_frame = medfilt2d(motion_frame.astype(np.uint8), kernel_size)
-    elif filtertype == 'Blob':
+    elif filtertype.lower() == 'blob':
         motion_frame = cv2.erode(motion_frame, np.ones(
             [kernel_size, kernel_size]), iterations=1)
     return motion_frame
diff --git a/mgmodule/_flow.py b/mgmodule/_flow.py
index a83ffea..366c235 100644
--- a/mgmodule/_flow.py
+++ b/mgmodule/_flow.py
@@ -1,16 +1,92 @@
 import cv2
 import os
 import numpy as np
-from ._utils import mg_progressbar
+from ._utils import mg_progressbar, extract_wav, embed_audio_in_video
 import mgmodule
 
 
 class Flow:
+    """
+    Class container for the sparse and dense optical flow processes.
 
-    def __init__(self, filename):
+    Attributes
+    ----------
+    - filename : str
+
+        Path to the input video file. Passed by parent MgObject.
+    - color : bool
+
+        Set class methods in color or grayscale mode. Passed by parent MgObject.
+
+    Methods
+    -------
+    - dense()
+
+        Renders a dense optical flow video of the input video file.
+    - sparse()
+
+        Renders a sparse optical flow video of the input video file.
+    """
+
+    def __init__(self, filename, color):
         self.filename = filename
+        self.color = color
+
+    def dense(
+            self,
+            filename='',
+            pyr_scale=0.5,
+            levels=3,
+            winsize=15,
+            iterations=3,
+            poly_n=5,
+            poly_sigma=1.2,
+            flags=0,
+            skip_empty=False):
+        """
+        Renders a dense optical flow video of the input video file using `cv2.calcOpticalFlowFarneback()`.
+        For more details about the parameters consult the cv2 documentation.
+
+        Parameters
+        ----------
+        - filename : str, optional
+
+            Path to the input video file. If not specified the video file pointed to by the MgObject is used.
+        - pyr_scale : float, optional
+
+            Default is 0.5.
+        - levels : int, optional
+
+            Default is 3.
+        - winsize : int, optional
+
+            Default is 15.
+        - iterations : int, optional
+
+            Default is 3.
+        - poly_n : int, optional
+
+            Default is 5.
+        - poly_sigma : float, optional
+
+            Default is 1.2.
+        - flags : int, optional
+
+            Default is 0.
+        - skip_empty : bool, optional
+
+            Default is `False`. If `True`, repeats previous frame in the output when encounters an empty frame.
+
+        Outputs
+        -------
+        - `filename`_flow_dense.avi
 
-    def dense(self, filename='', pyr_scale=0.5, levels=3, winsize=15, iterations=3, poly_n=5, poly_sigma=1.2, flags=0, skip_empty=False):
+        Returns
+        -------
+        - MgObject
+
+            A new MgObject pointing to the output '_flow_dense' video file.
+        """
 
         if filename == '':
             filename = self.filename
@@ -52,10 +128,13 @@ def dense(self, filename='', pyr_scale=0.5, levels=3, winsize=15, iterations=3,
                 if skip_empty:
                     if np.sum(rgb) > 0:
                         out.write(rgb.astype(np.uint8))
+                    else:
+                        out.write(prev_rgb.astype(np.uint8))
                 else:
                     out.write(rgb.astype(np.uint8))
 
                 prev_frame = next_frame
+                prev_rgb = rgb
 
             else:
                 mg_progressbar(
@@ -67,9 +146,66 @@ def dense(self, filename='', pyr_scale=0.5, levels=3, winsize=15, iterations=3,
             mg_progressbar(
                 ii, length+1, 'Rendering dense optical flow video:', 'Complete')
 
-        return mgmodule.MgObject(of + '_flow_dense' + fex)
+        out.release()
+        source_audio = extract_wav(of + fex)
+        destination_video = of + '_flow_dense' + fex
+        embed_audio_in_video(source_audio, destination_video)
+        os.remove(source_audio)
+
+        return mgmodule.MgObject(destination_video, color=self.color, returned_by_process=True)
+
+    def sparse(
+            self,
+            filename='',
+            corner_max_corners=100,
+            corner_quality_level=0.3,
+            corner_min_distance=7,
+            corner_block_size=7,
+            of_win_size=(15, 15),
+            of_max_level=2,
+            of_criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)):
+        """
+        Renders a sparse optical flow video of the input video file using `cv2.calcOpticalFlowPyrLK()`.
+        `cv2.goodFeaturesToTrack()` is used for the corner estimation.
+        For more details about the parameters consult the cv2 documentation.
+
+        Parameters
+        ----------
+        - filename : str, optional
+
+            Path to the input video file. If not specified the video file pointed to by the MgObject is used.
+        - corner_max_corners : int, optional
+
+            Default is 100.
+        - corner_quality_level : float, optional
+
+            Default is 0.3.
+        - corner_min_distance : int, optional
+
+            Default is 7.
+        - corner_block_size : int, optional
+
+            Default is 7.
+        - of_win_size : tuple (int, int), optional
 
-    def sparse(self, filename='', corner_max_corners=100, corner_quality_level=0.3, corner_min_distance=7, corner_block_size=7, of_win_size=(15, 15), of_max_level=2, of_criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)):
+            Default is (15, 15).
+        - of_max_level : int, optional
+
+            Default is 2.
+        - of_criteria : optional
+
+            Default is `(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)`.
+
+        Outputs
+        -------
+        - `filename`_flow_sparse.avi
+
+        Returns
+        -------
+        - MgObject
+
+            A new MgObject pointing to the output '_flow_sparse' video file.
+        """
 
         if filename == '':
             filename = self.filename
@@ -130,7 +266,13 @@ def sparse(self, filename='', corner_max_corners=100, corner_quality_level=0.3,
                     a, b = new.ravel()
                     c, d = old.ravel()
                     mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2)
-                    frame = cv2.circle(frame, (a, b), 5, color[i].tolist(), -1)
+
+                    if self.color == False:
+                        frame = cv2.cvtColor(frame_gray, cv2.COLOR_GRAY2BGR)
+
+                    frame = cv2.circle(
+                        frame, (a, b), 5, color[i].tolist(), -1)
+
                 img = cv2.add(frame, mask)
 
                 out.write(img.astype(np.uint8))
@@ -149,4 +291,10 @@ def sparse(self, filename='', corner_max_corners=100, corner_quality_level=0.3,
             mg_progressbar(
                 ii, length+1, 'Rendering sparse optical flow video:', 'Complete')
 
-        return mgmodule.MgObject(of + '_flow_sparse' + fex)
+        out.release()
+        source_audio = extract_wav(of + fex)
+        destination_video = of + '_flow_sparse' + fex
+        embed_audio_in_video(source_audio, destination_video)
+        os.remove(source_audio)
+
+        return mgmodule.MgObject(destination_video, color=self.color, returned_by_process=True)
diff --git a/mgmodule/_history.py b/mgmodule/_history.py
index bf345c3..d275b6f 100755
--- a/mgmodule/_history.py
+++ b/mgmodule/_history.py
@@ -1,26 +1,34 @@
 import cv2
 import os
 import numpy as np
-from ._utils import mg_progressbar
+from ._utils import mg_progressbar, extract_wav, embed_audio_in_video
 import mgmodule
 
 
-# added self, because this function is now called from an MgObject
 def history(self, filename='', history_length=10):
     """
-    This function  creates a video where each frame is the average of the n previous frames, where n is determined
-    from the history_length parameter.
-    The history frames are summed up and normalized, and added to the current frame to show the history. 
-    Outputs a video called filename + '_history.avi'.
+    This function  creates a video where each frame is the average of the 
+    n previous frames, where n is determined by `history_length`.
+    The history frames are summed up and normalized, and added to the 
+    current frame to show the history. 
 
     Parameters
     ----------
-    - filename (str): The video file to process.
-    - history_length (int): How many frames will be saved to the history tail.
+    - filename : str, optional
+
+        Path to the input video file. If not specified the video file pointed to by the MgObject is used.
+    - history_length : int, optional
+
+        Default is 10. Number of frames to be saved in the history tail.
+
+    Outputs
+    -------
+    - `filename`_history.avi
 
     Returns
     -------
-    - An MgObject loaded with the resulting _history video.
+    - MgObject 
+        A new MgObject pointing to the output '_history' video file.
     """
 
     if filename == '':
@@ -43,9 +51,13 @@ def history(self, filename='', history_length=10):
     history = []
 
     while(video.isOpened()):
+        ret, frame = video.read()
         if ret == True:
-            ret, frame = video.read()
+            if self.color == False:
+                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+
             frame = (np.array(frame)).astype(np.float64)
+
             if len(history) > 0:
                 history_total = frame/(len(history)+1)
             else:
@@ -59,15 +71,24 @@ def history(self, filename='', history_length=10):
             # 0.5 to not overload it poor thing
             total = history_total.astype(np.uint64)
 
-            out.write(total.astype(np.uint8))
+            if self.color == False:
+                total = cv2.cvtColor(total.astype(
+                    np.uint8), cv2.COLOR_GRAY2BGR)
+                out.write(total)
+            else:
+                out.write(total.astype(np.uint8))
 
         else:
-            #print('Rendering history 100%')
             mg_progressbar(
                 length, length, 'Rendering history video:', 'Complete')
             break
         ii += 1
-        #print('Rendering history %s%%' % (int(ii/(length-1)*100)), end='\r')
         mg_progressbar(ii, length+1, 'Rendering history video:', 'Complete')
 
-    return mgmodule.MgObject(of + '_history' + fex)
+    out.release()
+    source_audio = extract_wav(self.of + self.fex)
+    destination_video = self.of + '_history' + self.fex
+    embed_audio_in_video(source_audio, destination_video)
+    os.remove(source_audio)
+
+    return mgmodule.MgObject(destination_video, color=self.color, returned_by_process=True)
diff --git a/mgmodule/_motionhistory.py b/mgmodule/_motionhistory.py
index 52b5bcf..a2c238c 100755
--- a/mgmodule/_motionhistory.py
+++ b/mgmodule/_motionhistory.py
@@ -4,31 +4,59 @@
 from scipy.signal import medfilt2d
 from ._centroid import centroid
 from ._filter import filter_frame
-from ._utils import mg_progressbar
+from ._utils import mg_progressbar, extract_wav, embed_audio_in_video
 import mgmodule
 
-def mg_motionhistory(self, history_length = 10, kernel_size = 5, method = 'Diff', filtertype = 'Regular', thresh = 0.05, blur = 'None',inverted_motionhistory = False):
+
+def mg_motionhistory(
+        self,
+        history_length=10,
+        kernel_size=5,
+        filtertype='Regular',
+        thresh=0.05,
+        blur='None',
+        inverted_motionhistory=False):
     """
-    Finds the difference in pixel value from one frame to the next in an input video, and saves the difference frame to a history tail.
-    The history frames are summed up and normalized, and added to the current difference frame to show the history of motion.
-    Outputs a video called filename + '_motionhistory.avi'.
+    Finds the difference in pixel value from one frame to the next in an input video, 
+    and saves the difference frame to a history tail. The history frames are summed up 
+    and normalized, and added to the current difference frame to show the history of 
+    motion. 
 
     Parameters
     ----------
-    - history_length (int): How many frames will be saved to the history tail.
-    - kernel_size (int)  Size of structuring element.
-    - method (str): Currently 'Diff' is the only implemented method.
-    - filtertype (str): 'Regular', 'Binary', 'Blob' (see function filterframe).
-	- thresh (float): a number in [0,1]. Eliminates pixel values less than given threshold.
-    - blur (str): 'Average' to apply a blurring filter, 'None' otherwise.
-    - inverted_motionhistory (bool): Invert colors of motionhistory video.
+    - history_length : int, optional
+
+        Default is 10. Number of frames to be saved in the history tail.
+    - kernel_size : int, optional
+
+        Default is 5. Size of structuring element.
+    - filtertype : {'Regular', 'Binary', 'Blob'}, optional
+
+        `Regular` turns all values below `thresh` to 0.
+        `Binary` turns all values below `thresh` to 0, above `thresh` to 1.
+        `Blob` removes individual pixels with erosion method.
+    - thresh : float, optional
+
+        A number in the range of 0 to 1. Default is 0.05.
+        Eliminates pixel values less than given threshold.
+    - blur : {'None', 'Average'}, optional
+
+        `Average` to apply a 10px * 10px blurring filter, `None` otherwise.
+    - inverted_motionhistory : bool, optional
+
+        Default is `False`. If `True`, inverts colors of the motionhistory video.
+
+    Outputs
+    -------
+    - `filename`_motionhistory.avi
 
     Returns
     -------
-    - An MgObject loaded with the resulting _motionhistory video.
+    - MgObject
+
+        A new MgObject pointing to the output '_motionhistory' video file.
     """
-    enhancement = 1 #This can be adjusted to higher number to make motion more visible. Use with caution to not make it overflow.
-    self.method = method
+    enhancement = 1  # This can be adjusted to higher number to make motion more visible. Use with caution to not make it overflow.
     self.filtertype = filtertype
     self.thresh = thresh
     self.blur = blur
@@ -38,80 +66,98 @@ def mg_motionhistory(self, history_length = 10, kernel_size = 5, method = 'Diff'
     #of = os.path.splitext(self.filename)[0]
     fex = os.path.splitext(self.filename)[1]
     fourcc = cv2.VideoWriter_fourcc(*'MJPG')
-    out = cv2.VideoWriter(self.of + '_motionhistory' + fex,fourcc, self.fps, (self.width,self.height))
+    out = cv2.VideoWriter(self.of + '_motionhistory' + fex,
+                          fourcc, self.fps, (self.width, self.height))
 
     ii = 0
     history = []
 
     if self.color == False:
-    	frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
     while(vidcap.isOpened()):
-        prev_frame = frame
+        if self.blur.lower() == 'average':
+            prev_frame = cv2.blur(frame, (10, 10))
+        elif self.blur.lower() == 'none':
+            prev_frame = frame
+
         ret, frame = vidcap.read()
 
-        if ret==True:
-            if self.blur == 'Average':
-                frame = cv2.blur(frame,(10,10)) #The higher these numbers the more blur you get
+        if ret == True:
+            if self.blur.lower() == 'average':
+                # The higher these numbers the more blur you get
+                frame = cv2.blur(frame, (10, 10))
 
-            if self.color == True:
-                frame = frame
-            else:
+            if self.color == False:
                 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
             frame = (np.array(frame)).astype(np.float64)
 
-            if self.method == 'Diff':
-                if self.color == True:
-                    motion_frame_rgb = np.zeros([self.height,self.width,3])
-                    for i in range(frame.shape[2]):
-                        motion_frame = (np.abs(frame[:,:,i]-prev_frame[:,:,i])).astype(np.float64)
-                        motion_frame = filter_frame(motion_frame,self.filtertype,self.thresh,kernel_size)
-                        motion_frame_rgb[:,:,i] = motion_frame
-
-                    if len(history)>0:
-                        motion_history = frame/(len(history)+1)
-                    else:
-                        motion_history = frame
-
-                    for newframe in history:
-                            motion_history += newframe/(len(history)+1)
-                    if len(history) > history_length or len(history) == history_length: # or however long history you would like
-                        history.pop(0)# pop first frame
-                    history.append(motion_frame_rgb)
-                    motion_history = motion_history.astype(np.uint64) #0.5 to not overload it poor thing
-
+            if self.color == True:
+                motion_frame_rgb = np.zeros([self.height, self.width, 3])
+                for i in range(frame.shape[2]):
+                    motion_frame = (
+                        np.abs(frame[:, :, i]-prev_frame[:, :, i])).astype(np.float64)
+                    motion_frame = filter_frame(
+                        motion_frame, self.filtertype, self.thresh, kernel_size)
+                    motion_frame_rgb[:, :, i] = motion_frame
+
+                if len(history) > 0:
+                    motion_history = motion_frame_rgb/(len(history)+1)
                 else:
-                    motion_frame = (np.abs(frame-prev_frame)).astype(np.float64)
-                    motion_frame = filter_frame(motion_frame,self.filtertype,self.thresh,kernel_size)
-                    if len(history)>0:
-                        motion_history = frame/(len(history)+1)
-                    else:
-                        motion_history = frame
+                    motion_history = motion_frame_rgb
+
+                for newframe in history:
+                    motion_history += newframe/(len(history)+1)
+                # or however long history you would like
+                if len(history) > history_length or len(history) == history_length:
+                    history.pop(0)  # pop first frame
+                history.append(motion_frame_rgb)
+                motion_history = motion_history.astype(
+                    np.uint64)  # 0.5 to not overload it poor thing
+
+            else:  # self.color = False
+                motion_frame = (np.abs(frame-prev_frame)
+                                ).astype(np.float64)
+                motion_frame = filter_frame(
+                    motion_frame, self.filtertype, self.thresh, kernel_size)
+                if len(history) > 0:
+                    motion_history = motion_frame/(len(history)+1)
+                else:
+                    motion_history = motion_frame
 
-                    for newframe in history:
-                            motion_history += newframe/(len(history)+1)
+                for newframe in history:
+                    motion_history += newframe/(len(history)+1)
 
-                    if len(history) > history_length or len(history) == history_length: # or however long history you would like
-                        history.pop(0)# pop first frame
+                # or however long history you would like
+                if len(history) > history_length or len(history) == history_length:
+                    history.pop(0)  # pop first frame
 
-                    history.append(motion_frame)
-                    motion_history = motion_history.astype(np.uint64)
+                history.append(motion_frame)
+                motion_history = motion_history.astype(np.uint64)
 
             if self.color == False:
-                motion_history_rgb = cv2.cvtColor(motion_history.astype(np.uint8), cv2.COLOR_GRAY2BGR)
+                motion_history_rgb = cv2.cvtColor(
+                    motion_history.astype(np.uint8), cv2.COLOR_GRAY2BGR)
             else:
                 motion_history_rgb = motion_history
             if inverted_motionhistory:
-                out.write(cv2.bitwise_not(enhancement*motion_history_rgb.astype(np.uint8)))
+                out.write(cv2.bitwise_not(
+                    enhancement*motion_history_rgb.astype(np.uint8)))
             else:
                 out.write(enhancement*motion_history_rgb.astype(np.uint8))
         else:
-            #print('Rendering motion history video 100%%')
-            mg_progressbar(self.length, self.length, 'Rendering motion history video:', 'Complete')
+            mg_progressbar(self.length, self.length,
+                           'Rendering motion history video:', 'Complete')
             break
-        ii+=1
-        #print('Rendering motion history video %s%%' %(int(ii/(self.length-1)*100)), end='\r')
-        mg_progressbar(ii, self.length, 'Rendering motion history video:', 'Complete')
+        ii += 1
+        mg_progressbar(ii, self.length,
+                       'Rendering motion history video:', 'Complete')
+
+    out.release()
+    source_audio = extract_wav(self.of + self.fex)
+    destination_video = self.of + '_motionhistory' + self.fex
+    embed_audio_in_video(source_audio, destination_video)
+    os.remove(source_audio)
 
-    return mgmodule.MgObject(self.of + '_motionhistory' + fex)
+    return mgmodule.MgObject(destination_video, color=self.color, returned_by_process=True)
diff --git a/mgmodule/_motionvideo.py b/mgmodule/_motionvideo.py
index d6c0285..20b3fc6 100755
--- a/mgmodule/_motionvideo.py
+++ b/mgmodule/_motionvideo.py
@@ -1,175 +1,331 @@
-import cv2, os
+import mgmodule
+import matplotlib.pyplot as plt
+import cv2
+import os
 import numpy as np
+import pandas as pd
 from scipy.signal import medfilt2d
 from ._centroid import centroid
-from ._utils import mg_progressbar
+from ._utils import mg_progressbar, extract_wav, embed_audio_in_video, frame2ms
 from ._filter import filter_frame
-import matplotlib.pyplot as plt
 
-import mgmodule
 
-def mg_motionvideo(self, method = 'Diff', filtertype = 'Regular', thresh = 0.05, blur = 'None', kernel_size = 5,inverted_motionvideo = False, inverted_motiongram = False, unit = 'seconds',equalize_motiongram = True):
+def mg_motionvideo(
+        self,
+        filtertype='Regular',
+        thresh=0.05,
+        blur='None',
+        kernel_size=5,
+        inverted_motionvideo=False,
+        inverted_motiongram=False,
+        unit='seconds',
+        equalize_motiongram=True,
+        save_plot=True,
+        save_data=True,
+        data_format="csv",
+        save_motiongrams=True,
+        save_video=True):
     """
-    Finds the difference in pixel value from one frame to the next in an input video, and saves the frames into a new video.
-    Describes the motion in the recording.
-    Outputs a video called filename + '_motion.avi'.
+    Finds the difference in pixel value from one frame to the next in an input video, 
+    and saves the frames into a new video. Describes the motion in the recording.
 
     Parameters
     ----------
-    - kernel_size (int): Size of structuring element.
-    - method (str): Currently 'Diff' is the only implemented method.
-    - filtertype (str): 'Regular', 'Binary', 'Blob' (see function filter_frame)
-    - thresh (float): a number in [0,1]. Eliminates pixel values less than given threshold.
-    - blur (str): 'Average' to apply a blurring filter, 'None' otherwise.
-    - inverted_motiongram (bool): Invert colors of motionvideo
-    - inverted_motiongram (bool): Invert colors of motiongram
-    - unit (str) = Unit in QoM plot. 'seconds' or 'samples'
-    - equalize_motiongram (bool): Converts the motiongram to hsv-color space and flattens the value channel (v).
+    - filtertype : {'Regular', 'Binary', 'Blob'}, optional
+
+        `Regular` turns all values below `thresh` to 0.
+        `Binary` turns all values below `thresh` to 0, above `thresh` to 1.
+        `Blob` removes individual pixels with erosion method.
+    - thresh : float, optional
+
+        A number in the range of 0 to 1. Default is 0.05.
+        Eliminates pixel values less than given threshold.
+    - blur : {'None', 'Average'}, optional
+
+        `Average` to apply a 10px * 10px blurring filter, `None` otherwise.
+    - kernel_size : int, optional
+
+        Default is 5. Size of structuring element.
+    - inverted_motionvideo : bool, optional
+
+        Default is `False`. If `True`, inverts colors of the motion video.
+    - inverted_motiongram : bool, optional
+
+        Default is `False`. If `True`, inverts colors of the motiongrams.
+    - unit : {'seconds', 'samples'}, optional
+
+        Unit in QoM plot.
+    - equalize_motiongram : bool, optional
+
+        Default is `True`. If `True`, converts the motiongrams to hsv-color 
+        space and flattens the value channel (v).
+    - save_plot : bool, optional
+
+        Default is `True`. If `True`, outputs motion-plot.
+    - save_data : bool, optional
+
+        Default is `True`. If `True`, outputs motion-data.
+    - data_format : {'csv', 'tsv', 'txt'}, optional
+
+        Specifies format of motion-data.
+    - save_motiongrams : bool, optional
+
+        Default is `True`. If `True`, outputs motiongrams.
+    - save_video : bool, optional
+
+        Default is `True`. If `True`, outputs the motion video.
+
+    Outputs
+    -------
+    - `filename`_motion.avi
+
+        A video of the absolute difference between consecutive frames in the source video. 
+    - `filename`_motion_com_qom.png
+
+        A plot describing the centroid of motion and the quantity of motion in the source video.
+    - `filename`_mgx.png
+
+        A horizontal motiongram of the source video.
+    - `filename`_mgy.png
+
+        A vertical motiongram of the source video.
+    - `filename`_motion.csv
+
+        A text file containing the quantity of motion and the centroid of motion for each frame 
+        in the source video with timecodes in milliseconds. Available formats: csv, tsv, txt.
 
     Returns
     -------
-    - An MgObject loaded with the resulting _motion video.
+    - MgObject 
+
+        A new MgObject pointing to the output '_motion' video file. If `save_video=False`, it 
+        returns an MgObject pointing to the input video file.
     """
 
-    self.blur = blur
-    self.method = method
-    self.thresh = thresh
-    self.filtertype = filtertype
-
-    vidcap = cv2.VideoCapture(self.of+self.fex)
-    ret, frame = vidcap.read()
-
-    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
-    out = cv2.VideoWriter(self.of + '_motion' + self.fex,fourcc, self.fps, (self.width,self.height))
-    gramx = np.zeros([1,self.width,3])
-    gramy = np.zeros([self.height,1,3])
-    qom = np.array([]) #quantity of motion
-    com = np.array([]) #centroid of motion
-    ii = 0
-    if self.color == False:
-        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
-        gramx = np.zeros([1,self.width])
-        gramy = np.zeros([self.height,1])
-
-    while(vidcap.isOpened()):
-        if self.blur == 'Average':
-            prev_frame = cv2.blur(frame,(10,10))
-        elif self.blur == 'None':
-            prev_frame = frame
+    if save_plot | save_data | save_motiongrams | save_video:
+
+        self.blur = blur
+        self.thresh = thresh
+        self.filtertype = filtertype
 
+        vidcap = cv2.VideoCapture(self.of+self.fex)
         ret, frame = vidcap.read()
-        if ret==True:
-            if self.blur == 'Average':
-                frame = cv2.blur(frame,(10,10)) #The higher these numbers the more blur you get
-            elif self.blur == 'None':
-                frame = frame                   #No blur
-
-            if self.color == True:
-                frame = frame
-            else:
-                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
-            frame = np.array(frame)
-            frame = frame.astype(np.int32)
+        if save_video:
+            fourcc = cv2.VideoWriter_fourcc(*'MJPG')
+            out = cv2.VideoWriter(self.of + '_motion' + self.fex,
+                                  fourcc, self.fps, (self.width, self.height))
+
+        if save_motiongrams:
+            gramx = np.zeros([1, self.width, 3])
+            gramy = np.zeros([self.height, 1, 3])
+        if save_data | save_plot:
+            time = np.array([])  # time in ms
+            qom = np.array([])  # quantity of motion
+            com = np.array([])  # centroid of motion
+
+        ii = 0
+
+        if self.color == False:
+            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+            if save_motiongrams:
+                gramx = np.zeros([1, self.width])
+                gramy = np.zeros([self.height, 1])
+
+        while(vidcap.isOpened()):
+            if self.blur.lower() == 'average':
+                prev_frame = cv2.blur(frame, (10, 10))
+            elif self.blur.lower() == 'none':
+                prev_frame = frame
+
+            ret, frame = vidcap.read()
+            if ret == True:
+                if self.blur.lower() == 'average':
+                    # The higher these numbers the more blur you get
+                    frame = cv2.blur(frame, (10, 10))
+
+                if self.color == False:
+                    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+
+                frame = np.array(frame)
+                frame = frame.astype(np.int32)
 
-            if self.method == 'Diff':
                 if self.color == True:
-                    motion_frame_rgb = np.zeros([self.height,self.width,3])
+                    motion_frame_rgb = np.zeros(
+                        [self.height, self.width, 3])
 
                     for i in range(frame.shape[2]):
-                        motion_frame = (np.abs(frame[:,:,i]-prev_frame[:,:,i])).astype(np.uint8)
-                        motion_frame = filter_frame(motion_frame,self.filtertype,self.thresh,kernel_size)
-                        motion_frame_rgb[:,:,i] = motion_frame
+                        motion_frame = (
+                            np.abs(frame[:, :, i]-prev_frame[:, :, i])).astype(np.uint8)
+                        motion_frame = filter_frame(
+                            motion_frame, self.filtertype, self.thresh, kernel_size)
+                        motion_frame_rgb[:, :, i] = motion_frame
 
-                    movement_y = np.mean(motion_frame_rgb,axis=1).reshape(self.height,1,3)
-                    movement_x = np.mean(motion_frame_rgb,axis=0).reshape(1,self.width,3)
-                    gramy = np.append(gramy,movement_y,axis=1)
-                    gramx = np.append(gramx,movement_x,axis=0)
+                    if save_motiongrams:
+                        movement_y = np.mean(motion_frame_rgb, axis=1).reshape(
+                            self.height, 1, 3)
+                        movement_x = np.mean(
+                            motion_frame_rgb, axis=0).reshape(1, self.width, 3)
+                        gramy = np.append(gramy, movement_y, axis=1)
+                        gramx = np.append(gramx, movement_x, axis=0)
 
                 else:
-                    motion_frame = (np.abs(frame-prev_frame)).astype(np.uint8)
-                    motion_frame = filter_frame(motion_frame,self.filtertype,self.thresh,kernel_size)
+                    motion_frame = (
+                        np.abs(frame-prev_frame)).astype(np.uint8)
+                    motion_frame = filter_frame(
+                        motion_frame, self.filtertype, self.thresh, kernel_size)
 
-                    movement_y = np.mean(motion_frame,axis=1).reshape(self.height,1)
-                    movement_x = np.mean(motion_frame,axis=0).reshape(1,self.width)
-                    gramy = np.append(gramy,movement_y,axis=1)
-                    gramx = np.append(gramx,movement_x,axis=0)
+                    if save_motiongrams:
+                        movement_y = np.mean(
+                            motion_frame, axis=1).reshape(self.height, 1)
+                        movement_x = np.mean(
+                            motion_frame, axis=0).reshape(1, self.width)
+                        gramy = np.append(gramy, movement_y, axis=1)
+                        gramx = np.append(gramx, movement_x, axis=0)
 
-            elif self.method == 'OpticalFlow':
-                print('Optical Flow not implemented yet!')
+                if self.color == False:
+                    motion_frame = cv2.cvtColor(
+                        motion_frame, cv2.COLOR_GRAY2BGR)
+                    motion_frame_rgb = motion_frame
 
-            if self.color == False:
-                motion_frame = cv2.cvtColor(motion_frame, cv2.COLOR_GRAY2BGR)
-                motion_frame_rgb = motion_frame
-            if inverted_motionvideo:
-                out.write(cv2.bitwise_not(motion_frame_rgb.astype(np.uint8)))
+                if save_video:
+                    if inverted_motionvideo:
+                        out.write(cv2.bitwise_not(
+                            motion_frame_rgb.astype(np.uint8)))
+                    else:
+                        out.write(motion_frame_rgb.astype(np.uint8))
+
+                if save_plot | save_data:
+                    combite, qombite = centroid(motion_frame_rgb.astype(
+                        np.uint8), self.width, self.height)
+                    if ii == 0:
+                        time = frame2ms(ii, self.fps)
+                        com = combite.reshape(1, 2)
+                        qom = qombite
+                    else:
+                        time = np.append(time, frame2ms(ii, self.fps))
+                        com = np.append(com, combite.reshape(1, 2), axis=0)
+                        qom = np.append(qom, qombite)
             else:
-                out.write(motion_frame_rgb.astype(np.uint8))
 
-            combite, qombite = centroid(motion_frame_rgb.astype(np.uint8),self.width,self.height)
-            if ii == 0:
-                com = combite.reshape(1,2)
-                qom = qombite
+                mg_progressbar(self.length, self.length,
+                               pgbar_text, 'Complete')
+                break
+            ii += 1
+            pgbar_text = 'Rendering motion' + ", ".join(np.array(["-video", "-grams", "-plots", "-data"])[
+                np.array([save_video, save_motiongrams, save_plot, save_data])]) + ":"
+            mg_progressbar(ii, self.length,
+                           pgbar_text, 'Complete')
+
+        if save_motiongrams:
+            if self.color == False:
+                # Normalize before converting to uint8 to keep precision
+                gramx = gramx/gramx.max()*255
+                gramy = gramy/gramy.max()*255
+                gramx = cv2.cvtColor(gramx.astype(
+                    np.uint8), cv2.COLOR_GRAY2BGR)
+                gramy = cv2.cvtColor(gramy.astype(
+                    np.uint8), cv2.COLOR_GRAY2BGR)
+
+            gramx = (gramx-gramx.min())/(gramx.max()-gramx.min())*255.0
+            gramy = (gramy-gramy.min())/(gramy.max()-gramy.min())*255.0
+
+            if equalize_motiongram:
+                gramx = gramx.astype(np.uint8)
+                gramx_hsv = cv2.cvtColor(gramx, cv2.COLOR_BGR2HSV)
+                gramx_hsv[:, :, 2] = cv2.equalizeHist(gramx_hsv[:, :, 2])
+                gramx = cv2.cvtColor(gramx_hsv, cv2.COLOR_HSV2BGR)
+
+                gramy = gramy.astype(np.uint8)
+                gramy_hsv = cv2.cvtColor(gramy, cv2.COLOR_BGR2HSV)
+                gramy_hsv[:, :, 2] = cv2.equalizeHist(gramy_hsv[:, :, 2])
+                gramy = cv2.cvtColor(gramy_hsv, cv2.COLOR_HSV2BGR)
+
+            if inverted_motiongram:
+                cv2.imwrite(self.of+'_mgx.png',
+                            cv2.bitwise_not(gramx.astype(np.uint8)))
+                cv2.imwrite(self.of+'_mgy.png',
+                            cv2.bitwise_not(gramy.astype(np.uint8)))
             else:
-                com=np.append(com,combite.reshape(1,2),axis =0)
-                qom=np.append(qom,qombite)
+                cv2.imwrite(self.of+'_mgx.png', gramx.astype(np.uint8))
+                cv2.imwrite(self.of+'_mgy.png', gramy.astype(np.uint8))
+
+        if save_data:
+            save_txt(self.of, time, com, qom, self.width,
+                     self.height, data_format)
+
+        if save_plot:
+            plot_motion_metrics(self.of, self.fps, com, qom,
+                                self.width, self.height, unit)
+
+        if save_video:
+            out.release()
+            source_audio = extract_wav(self.of + self.fex)
+            destination_video = self.of + '_motion' + self.fex
+            embed_audio_in_video(source_audio, destination_video)
+            os.remove(source_audio)
+            return mgmodule.MgObject(destination_video, color=self.color, returned_by_process=True)
         else:
-            #print('Rendering motion video 100%')
-            mg_progressbar(self.length, self.length, 'Rendering motion video:', 'Complete')
-            break
-        ii+=1
-        #print('\rRendering motion video %s%%' %(int(ii/(self.length-1)*100)), end=" ")
-        mg_progressbar(ii, self.length, 'Rendering motion video:', 'Complete')
-    if self.color == False:
-        gramx = gramx/gramx.max()*255 #Normalize before converting to uint8 to keep precision
-        gramy = gramy/gramy.max()*255
-        gramx = cv2.cvtColor(gramx.astype(np.uint8), cv2.COLOR_GRAY2BGR)
-        gramy = cv2.cvtColor(gramy.astype(np.uint8), cv2.COLOR_GRAY2BGR)
-
-    gramx = (gramx-gramx.min())/(gramx.max()-gramx.min())*255.0
-    gramy = (gramy-gramy.min())/(gramy.max()-gramy.min())*255.0
-
-    if equalize_motiongram:
-        gramx = gramx.astype(np.uint8)
-        gramx_hsv = cv2.cvtColor(gramx, cv2.COLOR_BGR2HSV)
-        gramx_hsv[:,:,2] = cv2.equalizeHist(gramx_hsv[:,:,2])
-        gramx = cv2.cvtColor(gramx_hsv, cv2.COLOR_HSV2BGR)
- 
-        gramy = gramy.astype(np.uint8)
-        gramy_hsv = cv2.cvtColor(gramy, cv2.COLOR_BGR2HSV)
-        gramy_hsv[:,:,2] = cv2.equalizeHist(gramy_hsv[:,:,2])
-        gramy = cv2.cvtColor(gramy_hsv, cv2.COLOR_HSV2BGR)
-
-    if inverted_motiongram:
-        cv2.imwrite(self.of+'_mgx.png',cv2.bitwise_not(gramx.astype(np.uint8)))
-        cv2.imwrite(self.of+'_mgy.png',cv2.bitwise_not(gramy.astype(np.uint8)))
+            return mgmodule.MgObject(self.of + self.fex, color=self.color, returned_by_process=True)
+
     else:
-        cv2.imwrite(self.of+'_mgx.png',gramx.astype(np.uint8))
-        cv2.imwrite(self.of+'_mgy.png',gramy.astype(np.uint8))
-    plot_motion_metrics(self.of,self.fps,com,qom,self.width,self.height,unit)
-
-    return mgmodule.MgObject(self.of + '_motion' + self.fex)
-
-def plot_motion_metrics(of,fps,com,qom,width,height, unit):
-    plt.rc('text',usetex = False)
-    plt.rc('font',family='serif')
-    np.savetxt(of+'_motion.tsv',np.append(np.append(qom.reshape(qom.shape[0],1),(com[:,0]/width).reshape(com.shape[0],1),axis=1),(com[:,1]/height).reshape(com.shape[0],1),axis=1), delimiter = '\t')
-    fig = plt.figure(figsize = (12,6))
-    ax = fig.add_subplot(1,2,1)
-    ax.scatter(com[:,0]/width,com[:,1]/height,s=2)
-    ax.set_xlim((0,1))
-    ax.set_ylim((0,1))
+        print("Nothing to render. Exiting...")
+        return mgmodule.MgObject(self.of + self.fex, returned_by_process=True)
+
+
+def plot_motion_metrics(of, fps, com, qom, width, height, unit):
+    plt.rc('text', usetex=False)
+    plt.rc('font', family='serif')
+    fig = plt.figure(figsize=(12, 6))
+    ax = fig.add_subplot(1, 2, 1)
+    ax.scatter(com[:, 0]/width, com[:, 1]/height, s=2)
+    ax.set_xlim((0, 1))
+    ax.set_ylim((0, 1))
     ax.set_xlabel('Pixels normalized')
     ax.set_ylabel('Pixels normalized')
     ax.set_title('Centroid of motion')
-    ax = fig.add_subplot(1,2,2)
-    if unit == 'seconds':
+    ax = fig.add_subplot(1, 2, 2)
+    if unit.lower() == 'seconds':
         ax.set_xlabel('Time[seconds]')
     else:
         ax.set_xlabel('Time[samples]')
         fps = 1
     ax.set_ylabel('Pixels normalized')
     ax.set_title('Quantity of motion')
-    ax.bar(np.arange(len(qom)-1)/fps,qom[1:]/(width*height))
-    #ax.plot(qom[1:-1])
-    plt.savefig('%s_motion_com_qom.png'%of,format='png')
+    ax.bar(np.arange(len(qom)-1)/fps, qom[1:]/(width*height))
+    plt.savefig('%s_motion_com_qom.png' % of, format='png')
+
+
+def save_txt(of, time, com, qom, width, height, data_format):
+    def save_single_file(of, time, com, qom, width, height, data_format):
+        data_format = data_format.lower()
+        df = pd.DataFrame({'Time': time, 'Qom': qom, 'ComX': com.transpose()[
+                          0]/width, 'ComY': com.transpose()[1]/height})
+        if data_format == "tsv":
+            with open(of+'_motion.tsv', 'wb') as f:
+                f.write(b'Time\tQom\tComX\tComY\n')
+                np.savetxt(f, df.values, delimiter='\t',
+                           fmt=['%d', '%d', '%.15f', '%.15f'])
+        elif data_format == "csv":
+            df.to_csv(of+'_motion.csv', index=None)
+        elif data_format == "txt":
+            with open(of+'_motion.txt', 'wb') as f:
+                f.write(b'Time Qom ComX ComY\n')
+                np.savetxt(f, df.values, delimiter=' ',
+                           fmt=['%d', '%d', '%.15f', '%.15f'])
+        elif data_format not in ["tsv", "csv", "txt"]:
+            print(
+                f"Invalid data format: '{data_format}'.\nFalling back to '.csv'.")
+
+    if type(data_format) == str:
+        save_single_file(of, time, com, qom, width, height, data_format)
+
+    elif type(data_format) == list:
+        if all([item.lower() in ["csv", "tsv", "txt"] for item in data_format]):
+            data_format = list(set(data_format))
+            [save_single_file(of, time, com, qom, width, height, item)
+             for item in data_format]
+        else:
+            print(
+                f"Unsupported formats in {data_format}.\nFalling back to '.csv'.")
+            save_single_file(of, time, com, qom, width, height, "csv")
diff --git a/mgmodule/_show.py b/mgmodule/_show.py
index 8224c46..4b120f5 100644
--- a/mgmodule/_show.py
+++ b/mgmodule/_show.py
@@ -6,18 +6,19 @@
 
 def mg_show(self, filename=None, key=None):
     """
-
     This function simply plays the current vidcap VideoObject. The speed of the video playback 
     might not match the true fps due to non-optimized code. 
 
-    Parameters:
+    Parameters
+    ----------
+    - filename : str, optional
 
-    - filename (str) : If left empty, the current vidcap object is played. If filename is given,
-    this file is played instead. 
-    - key (str) : {'mgx', 'mgy', 'average', 'plot', 'motion', 'history', 'motionhistory', 'sparse', 'dense'} If either 
-    of the shorthands 'motion', 'history', 'motionhistory', 'sparse', or 'dense' is used the method attempts to show 
-    the (previously rendered) video file corresponding to the one in the MgObject.
+        Default is `None`. If `None`, the current video to which the MgObject points is played.
+        If filename is given, this file is played instead. 
+    - key : {None, 'mgx', 'mgy', 'average', 'plot', 'motion', 'history', 'motionhistory', 'sparse', 'dense'}, optional
 
+        If either of these shorthands is used the method attempts to show the 
+        (previously rendered) video file corresponding to the one in the MgObject.
     """
 
     video_mode = True
@@ -33,41 +34,41 @@ def show_image(ending, title=''):
 
         if key == None:
             filename = self.of+self.fex
-        elif key == 'mgx':
+        elif key.lower() == 'mgx':
             show_image('_mgx.png', 'Horizontal Motiongram')
-        elif key == 'mgy':
+        elif key.lower() == 'mgy':
             show_image('_mgy.png', 'Vertical Motiongram')
-        elif key == 'average':
+        elif key.lower() == 'average':
             show_image('_average.png', 'Average')
-        elif key == 'plot':
+        elif key.lower() == 'plot':
             show_image('_motion_com_qom.png',
                        'Centroid and Quantity of Motion')
 
-        elif key == 'motion':
+        elif key.lower() == 'motion':
             if os.path.exists(self.of + '_motion' + self.fex):
                 filename = self.of + '_motion' + self.fex
             else:
                 print("No motion video found corresponding to",
                       self.of+self.fex, ". Try making one with .motion()")
-        elif key == 'history':
+        elif key.lower() == 'history':
             if os.path.exists(self.of + '_history' + self.fex):
                 filename = self.of + '_history' + self.fex
             else:
                 print("No history video found corresponding to",
                       self.of+self.fex, ". Try making one with .history()")
-        elif key == 'motionhistory':
+        elif key.lower() == 'motionhistory':
             if os.path.exists(self.of + '_motionhistory' + self.fex):
                 filename = self.of + '_motionhistory' + self.fex
             else:
                 print("No motion history video found corresponding to",
                       self.of+self.fex, ". Try making one with .motionhistory()")
-        elif key == 'sparse':
+        elif key.lower() == 'sparse':
             if os.path.exists(self.of + '_flow_sparse' + self.fex):
                 filename = self.of + '_flow_sparse' + self.fex
             else:
                 print("No sparse optical flow video found corresponding to",
                       self.of+self.fex, ". Try making one with .flow.sparse()")
-        elif key == 'dense':
+        elif key.lower() == 'dense':
             if os.path.exists(self.of + '_flow_dense' + self.fex):
                 filename = self.of + '_flow_dense' + self.fex
             else:
diff --git a/mgmodule/_utils.py b/mgmodule/_utils.py
index 7799303..942f39f 100644
--- a/mgmodule/_utils.py
+++ b/mgmodule/_utils.py
@@ -1,17 +1,43 @@
-def mg_progressbar(iteration, total, prefix='', suffix='', decimals=1, length=40, fill='█', printEnd="\r"):
+
+
+def mg_progressbar(
+        iteration,
+        total,
+        prefix='',
+        suffix='',
+        decimals=1,
+        length=40,
+        fill='█',
+        printEnd="\r"):
     """
-    Call in a loop to create terminal progress bar.
+    Calls in a loop to create terminal progress bar.
+
+    Parameters
+    ----------
+    - iteration : int
+
+        Current iteration.
+    - total : int
+
+        Total iterations.
+    - prefix : str, optional
+
+        Prefix string.
+    - suffix : str, optional
+
+        Suffix string.
+    - decimals : int, optional
+
+        Default is 1. Positive number of decimals in percent complete.
+    - length : int, optional
+
+        Default is 40. Character length of bar.
+    - fill : str, optional
 
-    Parameters:
-    -----------
-    - iteration   - Required  : current iteration (Int)
-    - total       - Required  : total iterations (Int)
-    - prefix      - Optional  : prefix string (Str)
-    - suffix      - Optional  : suffix string (Str)
-    - decimals    - Optional  : positive number of decimals in percent complete (Int)
-    - length      - Optional  : character length of bar (Int)
-    - fill        - Optional  : bar fill character (Str)
-    - printEnd    - Optional  : end character (e.g. "\\r", "\\r\\n") (Str)
+        Default is '█'. Bar fill character.
+    - printEnd : str, optional.
+
+        Default is '\\r'. End of line character.
     """
     import sys
 
@@ -28,19 +54,117 @@ def mg_progressbar(iteration, total, prefix='', suffix='', decimals=1, length=40
 
 
 def scale_num(val, in_low, in_high, out_low, out_high):
-    """Scale a number linearly."""
+    """
+    Scales a number linearly.
+
+    Parameters
+    ----------
+    - val : int or float
+
+        The value to be scaled.
+    - in_low : int or float
+
+        Minimum of input range.
+    - in_high : int or float
+
+        Maximum of input range.
+    - out_low : int or float
+
+        Minimum of output range.
+    - out_high : int or float
+
+        Maximum of output range.
+
+    Returns
+    -------
+    int or float
+
+        The scaled number.
+    """
     return ((val - in_low) * (out_high - out_low)) / (in_high - in_low) + out_low
 
 
-def scale_array(array, new_min, new_max):
-    """Scale an array linearly."""
+def scale_array(array, out_low, out_high):
+    """
+    Scales an array linearly.
+
+    Parameters
+    ----------
+    - array : arraylike
+
+        The array to be scaled.
+    - out_low : int or float
+
+        Minimum of output range.
+    - out_high : int or float
+
+        Maximum of output range.
+
+    Returns
+    -------
+    - arraylike
+
+        The scaled array.
+    """
     minimum, maximum = np.min(array), np.max(array)
-    m = (new_max - new_min) / (maximum - minimum)
-    b = new_min - m * minimum
+    m = (out_high - out_low) / (maximum - minimum)
+    b = out_low - m * minimum
     return m * array + b
 
 
+def get_frame_planecount(frame):
+    """
+    Gets the planecount (color channel count) of a video frame.
+
+    Parameters
+    ----------
+    - frame : numpy array
+
+        A frame extracted by `cv2.VideoCapture().read()`.
+
+    Returns
+    -------
+    - {3, 1}
+
+        The planecount of the input frame.
+    """
+    import numpy as np
+    return 3 if len(np.array(frame).shape) == 3 else 1
+
+
+def frame2ms(frame, fps):
+    """
+    Converts frames to milliseconds.
+
+    Parameters
+    ----------
+    - frame : int
+
+        The index of the frame to be converted to milliseconds.
+    - fps : int
+
+        Frames per second.
+
+    Returns
+    -------
+    - int
+
+        The rounded millisecond value of the input frame index.
+    """
+    return round(frame / fps * 1000)
+
+
 class MgImage():
+    """
+    Class for handling images in the Motion Gestures Toolbox.
+
+    Attributes
+    ----------
+    - filename : str
+
+        The path to the image file to be loaded.
+    """
+
     def __init__(self, filename):
         self.filename = filename
         import os
@@ -48,11 +172,230 @@ def __init__(self, filename):
         self.fex = os.path.splitext(self.filename)[1]
     from ._show import mg_show as show
 
+    def __repr__(self):
+        return f"MgImage('{self.filename}')"
+
 
 def convert_to_avi(filename):
-    """Convert any video to one with .avi extension using ffmpeg"""
+    """
+    Converts a video to one with .avi extension using ffmpeg.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the input video file.
+
+    Outputs
+    -------
+    - `filename`.avi
+
+        The converted video file.
+
+    Returns
+    -------
+    - str
+
+        The path to the output '.avi' file.
+    """
     import os
     of = os.path.splitext(filename)[0]
     fex = os.path.splitext(filename)[1]
-    cmds = ' '.join(['ffmpeg', '-i', filename, of + '.avi'])
+    cmds = ' '.join(['ffmpeg', '-i', filename, "-c:v",
+                     "mjpeg", "-q:v", "3", of + '.avi'])
     os.system(cmds)
+    return of + '.avi'
+
+
+def rotate_video(filename, angle):
+    """
+    Rotates a video by an `angle` using ffmpeg.
+
+    Parameters
+    ----------
+    - filename : str
+
+        The path to the input video.
+    - angle : int or float
+
+        The angle (in degrees) specifying the amount of rotation. Positive values rotate clockwise.
+
+    Outputs
+    -------
+    - `filename`_rot.avi
+
+        The rotated video file.
+
+    Returns
+    -------
+    - str
+
+        The path to the output (rotated) video file.
+    """
+    import os
+    import math
+    of = os.path.splitext(filename)[0]
+    fex = os.path.splitext(filename)[1]
+    if os.path.isfile(of + '_rot.avi'):
+        os.remove(of + '_rot.avi')
+    cmds = ' '.join(['ffmpeg', '-i', filename, "-c:v",
+                     "mjpeg", "-q:v", "3", "-vf", f"rotate={math.radians(angle)}", of + '_rot.avi'])
+    os.system(cmds)
+    return of + '_rot', fex
+
+
+def convert_to_grayscale(filename):
+    """
+    Converts a video to grayscale using ffmpeg.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the video file to be converted to grayscale.
+
+    Outputs
+    -------
+    - `filename`_gray.avi
+
+    Returns
+    -------
+    - str
+
+        The path to the output (grayscale) video file.
+    """
+    import os
+    of = os.path.splitext(filename)[0]
+    fex = os.path.splitext(filename)[1]
+    cmds = ' '.join(['ffmpeg', '-i', filename, "-c:v", "mjpeg", "-q:v", "3", '-vf',
+                     'hue=s=0', of + '_gray' + fex])
+    os.system(cmds)
+    return of + '_gray', fex
+
+
+def extract_wav(filename):
+    """
+    Extracts audio from video into a .wav file via ffmpeg.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the video file from which the audio track shall be extracted.
+
+    Outputs
+    -------
+    - `filename`.wav
+
+    Returns
+    -------
+    - str
+
+        The path to the output audio file.
+    """
+    import os
+    of = os.path.splitext(filename)[0]
+    fex = os.path.splitext(filename)[1]
+    cmds = ' '.join(['ffmpeg', '-i', filename, "-acodec",
+                     "pcm_s16le", of + '.wav'])
+    os.system(cmds)
+    return of + '.wav'
+
+
+def get_length(filename):
+    """
+    Gets the length (s) of a video using ffprobe.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the video file to be measured.
+
+    Returns
+    -------
+    - float
+
+        The length of the input video file in seconds.
+    """
+    import subprocess
+    result = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of',
+                             'default=noprint_wrappers=1:nokey=1', filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    return float(result.stdout)
+
+
+def audio_dilate(filename, dilation_ratio=1):
+    """
+    Time-stretches or -shrinks (dilates) an audio file using ffmpeg.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the audio file to be dilated.
+    - dilation_ratio : int or float, optional
+
+        Default is 1. The source file's length divided by the resulting file's length.
+
+    Outputs
+    -------
+    - `filename`_dilated.wav
+
+    Returns
+    -------
+    - str
+
+        The path to the output audio file.
+    """
+    import os
+    of = os.path.splitext(filename)[0]
+    fex = os.path.splitext(filename)[1]
+    cmds = ' '.join(['ffmpeg', '-i', filename, '-codec:a', 'pcm_s16le',
+                     '-filter:a', 'atempo=' + str(dilation_ratio), of + '_dilated' + fex])
+    os.system(cmds)
+    return of + '_dilated' + fex
+
+
+def embed_audio_in_video(source_audio, destination_video, dilation_ratio=1):
+    """
+    Embeds an audio file as the audio channel of a video file using ffmpeg.
+
+    Parameters
+    ----------
+    - source_audio : str
+
+        Path to the audio file to be embedded.
+
+    - destination_video : str
+
+        Path to the video file to embed the audio file in.
+
+    Outputs
+    -------
+    - `destination_video` with the embedded audio file.
+    """
+    import os
+    of = os.path.splitext(destination_video)[0]
+    fex = os.path.splitext(destination_video)[1]
+
+    # dilate audio file if necessary (ie. when skipping)
+    if dilation_ratio != 1:
+        audio_to_embed = audio_dilate(
+            source_audio, dilation_ratio)  # creates '_dilated.wav'
+        dilated = True
+    else:
+        audio_to_embed = source_audio
+        dilated = False
+
+    # embed audio in video
+    cmds = ' '.join(['ffmpeg', '-i', destination_video, '-i', audio_to_embed, '-c:v',
+                     'copy', '-map', '0:v:0', '-map', '1:a:0', '-shortest', of + '_w_audio' + fex])
+    os.system(cmds)  # creates '_w_audio.avi'
+
+    # cleanup:
+    # if we needed to create an additional (dilated) audio file, delete it
+    if dilated:
+        os.remove(audio_to_embed)
+    # replace (silent) destination_video with the one with the embedded audio
+    os.remove(destination_video)
+    os.rename(of + '_w_audio' + fex, destination_video)
diff --git a/mgmodule/_videoadjust.py b/mgmodule/_videoadjust.py
index 0f31237..0bd049d 100755
--- a/mgmodule/_videoadjust.py
+++ b/mgmodule/_videoadjust.py
@@ -5,20 +5,45 @@
 
 def mg_contrast_brightness(of, fex, vidcap, fps, length, width, height, contrast, brightness):
     """
-    Edit contrast and brightness of the video.
+    Applies contrast and brightness to a video.
 
-    Arguments
-    ---------
-    - of (str): 'Only filename' without extension.
-    - fex (str): File extension.
-    - vidcap: cv2 capture of video file, with all frames ready to be read with vidcap.read().
-    - fps, width, height (int): Properties of vidcap, passed by parent function.
-    - contrast (float): Apply +/- 100 contrast to video.
-    - brightness (float): Apply +/- 100 brightness to video.
+    Parameters
+    ----------
+    - of : str
+
+        'Only filename' without extension (but with path to the file).
+    - fex : str
+
+        File extension.
+    - vidcap : 
+
+        cv2 capture of video file, with all frames ready to be read with `vidcap.read()`.
+    - fps : int
+
+        The FPS (frames per second) of the input video capture.
+    - length : int
+
+        The number of frames in the input video capture.
+    - width : int
+
+        The pixel width of the input video capture. 
+    - height : int
+
+        The pixel height of the input video capture. 
+    - contrast : int or float, optional
+
+        Applies +/- 100 contrast to video.
+    - brightness : int or float, optional
+
+        Applies +/- 100 brightness to video.
+
+    Outputs
+    -------
+    - A video file with the name `of` + '_cb' + `fex`.
 
     Returns
     -------
-    - cv2 video capture of edited video file.
+    - cv2 video capture of output video file.
     """
 
     count = 0
@@ -36,7 +61,6 @@ def mg_contrast_brightness(of, fex, vidcap, fps, length, width, height, contrast
         while success:
             success, image = vidcap.read()
             if not success:
-                #print('Adjusting contrast/brightness 100%%')
                 mg_progressbar(
                     length, length, 'Adjusting contrast and brightness:', 'Complete')
                 break
@@ -44,8 +68,6 @@ def mg_contrast_brightness(of, fex, vidcap, fps, length, width, height, contrast
             image = np.clip(image, 0, 255)
             out.write(image.astype(np.uint8))
             count += 1
-            # print('Adjusting contrast/brightness %s%%' %
-            #       (int(count/(length-1)*100)), end='\r')
             mg_progressbar(
                 count, length, 'Adjusting contrast and brightness:', 'Complete')
         out.release()
@@ -54,22 +76,58 @@ def mg_contrast_brightness(of, fex, vidcap, fps, length, width, height, contrast
     return vidcap
 
 
-def mg_skip_frames(of, fex, vidcap, skip, fps, width, height):
+def mg_skip_frames(of, fex, vidcap, skip, fps, length, width, height):
     """
-    Frame skip, convenient for saving time/space in an analysis of less detail looking at big picture movement. Skips the given number of frames, making a compressed version of the input video file.
+    Time-shrinks the video by skipping (discarding) every n frames determined by `skip`.
+
+    Parameters
+    ----------
+    - of : str
+
+        'Only filename' without extension (but with path to the file).
+    - fex : str
+
+        File extension.
+    - vidcap : 
+
+        cv2 capture of video file, with all frames ready to be read with `vidcap.read()`.
+    - skip : int
 
-    Arguments
-    ---------
-    - of (str): 'Only filename' without extension.
-    - fex (str): File extension.
-    - vidcap: cv2 capture of video file, with all frames ready to be read with vidcap.read().
-    - skip (int): When proceeding to analyze next frame of video, this many frames are skipped.
-    - fps, width, height (int): Properties of vidcap, passed by parent function.
+        Every n frames to discard. `skip=0` keeps all frames, `skip=1` skips every other frame.
+    - fps : int
+
+        The FPS (frames per second) of the input video capture.
+    - length : int
+
+        The number of frames in the input video capture.
+    - width : int
+
+        The pixel width of the input video capture. 
+    - height : int
+
+        The pixel height of the input video capture.
+
+    Outputs
+    -------
+    - A video file with the name `of` + '_skip' + `fex`.
 
     Returns
     -------
-    - cv2 video capture of edited video file.
-    - length, fps, width, height from this video capture.
+    - videcap :
+
+        cv2 video capture of output video file.
+    - length : int
+
+        The number of frames in the output video file.
+    - fps : int
+
+        The FPS (frames per second) of the output video file.
+    - width : int
+
+        The pixel width of the output video file. 
+    - height : int
+
+        The pixel height of the output video file. 
     """
     count = 0
     if skip != 0:
@@ -80,12 +138,15 @@ def mg_skip_frames(of, fex, vidcap, skip, fps, width, height):
         while success:
             success, image = vidcap.read()
             if not success:
+                mg_progressbar(
+                    length, length, 'Skipping frames:', 'Complete')
                 break
             # on every frame we wish to use
             if (count % (skip+1) == 0):  # NB if skip=1, we should keep every other frame
                 out.write(image.astype(np.uint8))
-
             count += 1
+            mg_progressbar(
+                count, length, 'Skipping frames:', 'Complete')
         out.release()
         vidcap = cv2.VideoCapture(of + '_skip' + fex)
 
diff --git a/mgmodule/_videoreader.py b/mgmodule/_videoreader.py
index 2106599..8e03e03 100755
--- a/mgmodule/_videoreader.py
+++ b/mgmodule/_videoreader.py
@@ -4,29 +4,107 @@
 import numpy as np
 from ._videoadjust import mg_contrast_brightness, mg_skip_frames
 from ._cropvideo import *
-from ._utils import convert_to_avi
+from ._utils import convert_to_avi, rotate_video, extract_wav, embed_audio_in_video, convert_to_grayscale
 
 
-def mg_videoreader(filename, starttime=0, endtime=0, skip=0, contrast=0, brightness=0, crop='None', keep_all=False):
+class ReadError(Exception):
+    """Base class for file read errors."""
+    pass
+
+
+def mg_videoreader(
+        filename,
+        starttime=0,
+        endtime=0,
+        skip=0,
+        rotate=0,
+        contrast=0,
+        brightness=0,
+        crop='None',
+        color=True,
+        keep_all=False,
+        returned_by_process=False):
     """
-        Reads in a video file, and by input parameters user decide if it: trims the length, skips frames, applies contrast/brightness adjustments and/or crops image width/height.
-
-        Arguments:
-        ----------
-        - filename (str): Name of input parameter video file.
-        - starttime (float): Cut the video from this start time (min) to analyze what is relevant.
-        - endtime (float): Cut the video at this end time (min) to analyze what is relevant.
-        - skip (int): When proceeding to analyze next frame of video, this many frames are skipped.
-        - contrast (float): Apply +/- 100 contrast to video
-        - brightness (float): Apply +/- 100 brightness to video
-        - crop (str): 'None', 'Auto' or 'Manual' to crop video.
-        - keep_all (bool): If False, only the result of the final process in the chain is kept, if True all results are kept.
-
-        Returns:
-        --------
-        - vidcap: cv2 video capture of edited video file
-        - length, fps, width, height from vidcap
-        - of: filename gets updated with what procedures it went through
+    Reads in a video file, and optionally apply several different processes on it. These include:
+    - trimming,
+    - skipping,
+    - rotating,
+    - applying brightness and contrast,
+    - cropping,
+    - converting to grayscale.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the input video file.
+    - starttime : int or float, optional
+
+        Trims the video from this start time (s).
+
+    - endtime : int or float, optional
+
+        Trims the video until this end time (s).
+
+    - skip : int, optional
+
+        Time-shrinks the video by skipping (discarding) every n frames determined by `skip`.
+    - rotate : int or float, optional
+
+        Rotates the video by a `rotate` degrees.
+
+    - contrast : int or float, optional
+
+        Applies +/- 100 contrast to video.
+    - brightness : int or float, optional
+
+        Applies +/- 100 brightness to video.
+
+    - crop : {'none', 'manual', 'auto'}, optional
+
+        If `manual`, opens a window displaying the first frame of the input video file,
+        where the user can draw a rectangle to which cropping is applied.
+        If `auto` the cropping function attempts to determine the area of significant motion 
+        and applies the cropping to that area.
+
+    - color : bool, optional
+
+        Default is `True`. If `False`, converts the video to grayscale and sets every method in grayscale mode.
+    - keep_all : bool, optional
+
+        Default is `False`. If `True`, preserves an output video file after each used preprocessing stage.
+
+    Outputs
+    -------
+    - A video file with the applied processes. The name of the file will be `filename` + a suffix for each process.
+
+    Returns
+    -------
+    - length : int
+
+        The number of frames in the output video file.
+
+    - width : int
+
+        The pixel width of the output video file. 
+    - height : int
+
+        The pixel height of the output video file. 
+    - fps : int
+
+        The FPS (frames per second) of the output video file.
+    - endtime : float
+
+        The length of the output video file in seconds.
+
+    - of: str
+
+        The path to the output video file without its extension.
+        The file name gets a suffix for each used process.
+    - fex : str
+
+        The file extension of the output video file.
+        Currently it is always 'avi'.
     """
     # Separate filename from file extension
     of = os.path.splitext(filename)[0]
@@ -34,6 +112,7 @@ def mg_videoreader(filename, starttime=0, endtime=0, skip=0, contrast=0, brightn
 
     trimming = False
     skipping = False
+    rotating = False
     cbing = False
     cropping = False
 
@@ -45,8 +124,10 @@ def mg_videoreader(filename, starttime=0, endtime=0, skip=0, contrast=0, brightn
 
     # Cut out relevant bit of video using starttime and endtime
     if starttime != 0 or endtime != 0:
+        print("Trimming...")
         trimvideo = ffmpeg_extract_subclip(
             filename, starttime, endtime, targetname=of + '_trim' + fex)
+        print("Trimming done.")
         of = of + '_trim'
         trimming = True
         vidcap = cv2.VideoCapture(of+fex)
@@ -61,34 +142,91 @@ def mg_videoreader(filename, starttime=0, endtime=0, skip=0, contrast=0, brightn
     height = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT))
     length = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
 
+    if fps == 0:
+        raise ReadError(f"Could not open {filename}.")
+
+    source_length_s = length / fps
+    source_name = of + fex
+    new_length_s = source_length_s
+    dilation_ratio = 1
+    need_to_embed_audio = False
+
+    if skip != 0 or contrast != 0 or brightness != 0 or crop.lower() != 'none':
+        source_audio = extract_wav(source_name)
+        need_to_embed_audio = True
+
     # To skip ahead a few frames before the next sample set skip to a value above 0
     if skip != 0:
         vidcap, length, fps, width, height = mg_skip_frames(
-            of, fex, vidcap, skip, fps, width, height)
+            of, fex, vidcap, skip, fps, length, width, height)
         if not keep_all and trimming:
             os.remove(of + fex)
         of = of + '_skip'
         skipping = True
+        new_length_s = length / fps
+        dilation_ratio = source_length_s / new_length_s
+        if keep_all:
+            vidcap.release()
+            embed_audio_in_video(source_audio, of + fex, dilation_ratio)
 
     # Overwrite the inputvalue for endtime not to cut the video at 0...
     if endtime == 0:
         endtime = length/fps
 
+    if rotate != 0:
+        vidcap.release()
+        print(f"Rotating video by {rotate} degrees...", end='')
+        rotate_video(of + fex, rotate)
+        print(" done.")
+        if not keep_all and (skipping or trimming):
+            os.remove(of + fex)
+        of = of + '_rot'
+        rotating = True
+        if keep_all:
+            embed_audio_in_video(source_audio, of + fex, dilation_ratio)
+
     # Apply contrast/brightness before the motion analysis
     if contrast != 0 or brightness != 0:
+        if keep_all or rotating:
+            vidcap = cv2.VideoCapture(of + fex)
         vidcap = mg_contrast_brightness(
             of, fex, vidcap, fps, length, width, height, contrast, brightness)
-        if not keep_all and (skipping or trimming):
+        if not keep_all and (rotating or skipping or trimming):
             os.remove(of + fex)
         of = of + '_cb'
         cbing = True
+        if keep_all:
+            vidcap.release()
+            embed_audio_in_video(source_audio, of + fex, dilation_ratio)
 
     # Crops video either manually or automatically
-    if crop != 'None':
+    if crop.lower() != 'none':
+        if keep_all:
+            vidcap = cv2.VideoCapture(of + fex)
         [vidcap, width, height] = mg_cropvideo(
             fps, width, height, length, of, fex, crop, motion_box_thresh=0.1, motion_box_margin=1)
-        if not keep_all and (cbing or skipping or trimming):
+        if not keep_all and (cbing or rotating or skipping or trimming):
             os.remove(of + fex)
         of = of + '_crop'
+        cropping = True
+        if keep_all:
+            vidcap.release()
+            embed_audio_in_video(source_audio, of + fex, dilation_ratio)
+
+    if color == False and returned_by_process == False:
+        vidcap.release()
+        print("Converting to grayscale...", end='')
+        of_gray, fex = convert_to_grayscale(of + fex)
+        print(" done.")
+        if not keep_all and (cropping or cbing or rotating or skipping or trimming):
+            os.remove(of + fex)
+        of = of_gray
+
+    if color == True or returned_by_process == True:
+        vidcap.release()
+
+    if need_to_embed_audio:
+        embed_audio_in_video(source_audio, of + fex, dilation_ratio)
+        os.remove(source_audio)
 
-    return vidcap, length, width, height, fps, endtime, of, fex
+    return length, width, height, fps, endtime, of, fex


Test patch:
diff --git a/examples/test_dance.py b/examples/test_dance.py
index e3a69b4..bbe1b81 100755
--- a/examples/test_dance.py
+++ b/examples/test_dance.py
@@ -5,29 +5,24 @@
 import mgmodule
 import cv2
 
-#import any non-module functions you want to use (see documentation) this way:
-from _average import average_image
-from _history import history
+# CREATE MODULE OBJECT: Here is an example call to create an mg Object, using loads of parameters
+mg = mgmodule.MgObject('../dance.avi', starttime=2,
+                       endtime=20, contrast=100, brightness=50)
 
-#CREATE MODULE OBJECT: Here is an example call to create an mg Object, using loads of parameters
-mg = mgmodule.MgObject('dance.avi', starttime = 2, endtime = 20, color = True, contrast = 100, brightness = 50)
+# USE MODULE METHOD: To run the motionvideo analysis, run the function using your object
+mg.motion(inverted_motionvideo=False, inverted_motiongram=False,
+          thresh=0.05, unit='seconds')
 
-#USE MODULE METHOD: To run the motionvideo analysis, run the function using your object
-mg.mg_motionvideo(inverted_motionvideo = False, inverted_motiongram = False, thresh=0.05, unit='seconds')
-
-#This runs the motion history on the motion video
-#mg.mg_motionhistory(history_length=50, thresh=0.05, inverted_motionhistory = False, blur='Average')
-
-history(mg.of+'.avi', history_length=25)
-history(mg.of+'_motion.avi', history_length=25)
-
-#USE NON-MODULE FUNCTION, this one can find an average image of any video, here using the mb objects filename
+# History video
+mg.history(history_length=25)
+# Motion history video
+mg.history(mg.of+'_motion.avi', history_length=25)
 
 # Average image of original video
-#average_image(mg.filename)
+# mg.average('../dance.avi')
 
 # Average image of pre-processed video
-average_image(mg.of+'.avi')
+mg.average()
 
 # Average image of motion video
-average_image(mg.of+'_motion.avi')
+mg.average(mg.of+'_motion.avi')
diff --git a/examples/test_pianist.py b/examples/test_pianist.py
index ea61698..ea8777a 100755
--- a/examples/test_pianist.py
+++ b/examples/test_pianist.py
@@ -5,24 +5,18 @@
 import mgmodule
 import cv2
 
-#import any non-module functions you want to use (see documentation) this way:
-from _average import average_image
-from _history import history
-
-#CREATE MODULE OBJECT: Here is an example call to create an mg Object, using loads of parameters
-mg = mgmodule.MgObject('pianist.avi', color=False, crop='auto', skip = 3)
-#USE MODULE METHOD: To run the motionvideo analysis, run the function using your object
-mg.mg_motionvideo(inverted_motionvideo = True, inverted_motiongram = True, thresh=0.1, blur='Average', normalize=False)
-#This runs the motion history on the motion video
-mg.mg_motionhistory(history_length=25, thresh=0.1, inverted_motionhistory = True, blur='Average')
-
-#USE NON-MODULE FUNCTION, this one can find an average image of any video, here using the mb objects filename
+# CREATE MODULE OBJECT: Here is an example call to create an mg Object, using loads of parameters
+mg = mgmodule.MgObject('pianist.avi', color=False, crop='auto', skip=3)
+# USE MODULE METHOD: To run the motionvideo analysis, run the function using your object,
+# then create the motion history by chaining the history() function onto the result of the previous (motion) function
+mg.motion(inverted_motionvideo=True, inverted_motiongram=True,
+          thresh=0.1, blur='Average').history(history_length=25)
 
 # Average image of original video
-#average_image(mg.filename)
+# mg.average('pianist.avi')
 
 # Average image of pre-processed video
-average_image(mg.of+'.avi')
+mg.average()
 
 # Average image of motion video
-average_image(mg.of+'_motion.avi')
+mg.average(mg.of+'_motion.avi')
diff --git a/mgmodule/_input_test.py b/mgmodule/_input_test.py
index 2f8c59b..9279772 100755
--- a/mgmodule/_input_test.py
+++ b/mgmodule/_input_test.py
@@ -6,30 +6,59 @@ class Error(Exception):
 class InputError(Error):
     """Exception raised for errors in the input.
 
-    Attributes:
-        message -- explanation of the error
+    Attributes
+    ----------
+    - message : str
+
+        Explanation of the error.
     """
 
     def __init__(self, message):
         self.message = message
 
 
-def mg_input_test(filename, method, filtertype, thresh, starttime, endtime, blur, skip):
-    """ Gives feedback to user if initialization from input went wrong. """
+def mg_input_test(filename, filtertype, thresh, starttime, endtime, blur, skip):
+    """
+    Gives feedback to user if initialization from input went wrong.
+
+    Parameters
+    ----------
+    - filename : str
+
+        Path to the input video file.
+    - filtertype : {'Regular', 'Binary', 'Blob'}
+
+        `Regular` turns all values below `thresh` to 0.
+        `Binary` turns all values below `thresh` to 0, above `thresh` to 1.
+        `Blob` removes individual pixels with erosion method.
+    - thresh : float
+
+        A number in the range of 0 to 1. Default is 0.05.
+        Eliminates pixel values less than given threshold.
+    - starttime : int or float
+
+        Trims the video from this start time (s).
+
+    - endtime : int or float
+
+        Trims the video until this end time (s).
+    - blur : {'None', 'Average'}
+
+        `Average` to apply a 10px * 10px blurring filter, `None` otherwise.
+    - skip : int
+
+        Every n frames to discard. `skip=0` keeps all frames, `skip=1` skips every other frame.
+    """
 
     filenametest = type(filename) == str
 
     if filenametest:
-        if method not in ['Diff', 'OpticalFlow']:
-            msg = 'Please specify a method for motion estimation as str: Diff or OpticalFlow.'
-            raise InputError(msg)
-
-        if filtertype not in ['Regular', 'Binary', 'Blob']:
-            msg = 'Please specify a filter type as str: Regular or Binary'
+        if filtertype.lower() not in ['regular', 'binary', 'blob']:
+            msg = 'Please specify a filter type as str: "Regular", "Binary" or "Blob"'
             raise InputError(msg)
 
-        if blur not in ['Average', 'None']:
-            msg = 'Please specify a blur type as str: Average or None'
+        if blur.lower() not in ['average', 'none']:
+            msg = 'Please specify a blur type as str: "Average" or "None"'
             raise InputError(msg)
 
         if not isinstance(thresh, (float, int)):