├── tests
├── __init__.py
└── flow_tests.py
├── static
├── Dimetrodon.flo
├── example_rgb.png
├── example_arrows.png
├── kitti_000010_10.png
├── kitti_000010_11.png
├── example_forward_warp.png
├── kitti_noc_000010_10.png
├── kitti_occ_000010_10.png
└── example_backward_warp.png
├── examples
├── flow_rgb.py
├── flow_with_arrows_tooltip_and_calibration.py
├── flow_forward_warp.py
└── flow_backward_warp.py
├── setup.py
├── flowpy
├── __init__.py
├── flow_io.py
└── flowpy.py
├── LICENSE
├── .gitignore
├── README.md
├── docs
├── index.html
├── flow_io.html
└── flowpy.html
└── scripts
└── flowread
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/Dimetrodon.flo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/Dimetrodon.flo
--------------------------------------------------------------------------------
/static/example_rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/example_rgb.png
--------------------------------------------------------------------------------
/static/example_arrows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/example_arrows.png
--------------------------------------------------------------------------------
/static/kitti_000010_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/kitti_000010_10.png
--------------------------------------------------------------------------------
/static/kitti_000010_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/kitti_000010_11.png
--------------------------------------------------------------------------------
/static/example_forward_warp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/example_forward_warp.png
--------------------------------------------------------------------------------
/static/kitti_noc_000010_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/kitti_noc_000010_10.png
--------------------------------------------------------------------------------
/static/kitti_occ_000010_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/kitti_occ_000010_10.png
--------------------------------------------------------------------------------
/static/example_backward_warp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickaelseznec/flowpy/HEAD/static/example_backward_warp.png
--------------------------------------------------------------------------------
/examples/flow_rgb.py:
--------------------------------------------------------------------------------
1 | import flowpy
2 | import matplotlib.pyplot as plt
3 |
4 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
5 |
6 | fig, ax = plt.subplots()
7 | ax.imshow(flowpy.flow_to_rgb(flow))
8 | plt.show()
9 |
--------------------------------------------------------------------------------
/examples/flow_with_arrows_tooltip_and_calibration.py:
--------------------------------------------------------------------------------
1 | import flowpy
2 | import matplotlib.pyplot as plt
3 |
4 | flow = flowpy.flow_read("static/Dimetrodon.flo")
5 | height, width, _ = flow.shape
6 |
7 | image_ratio = height / width
8 | max_radius = flowpy.get_flow_max_radius(flow)
9 |
10 | fig, (ax_1, ax_2) = plt.subplots(
11 | 1, 2, gridspec_kw={"width_ratios": [1, image_ratio]}
12 | )
13 |
14 | ax_1.imshow(flowpy.flow_to_rgb(flow))
15 | flowpy.attach_arrows(ax_1, flow)
16 | flowpy.attach_coord(ax_1, flow)
17 |
18 | flowpy.attach_calibration_pattern(ax_2, flow_max_radius=max_radius)
19 |
20 | plt.show()
21 |
--------------------------------------------------------------------------------
/examples/flow_forward_warp.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 |
4 | from PIL import Image
5 |
6 | import flowpy
7 |
8 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
9 | first_image = np.asarray(Image.open("static/kitti_000010_10.png"))
10 | second_image = np.asarray(Image.open("static/kitti_000010_11.png"))
11 |
12 | flow[np.isnan(flow)] = 0
13 | warped_second_image = flowpy.forward_warp(first_image, flow)
14 |
15 | fig, ax = plt.subplots()
16 |
17 | ax.imshow(warped_second_image)
18 | ax.set_title( "First image warped to the second")
19 | ax.set_axis_off()
20 |
21 | plt.show()
22 |
23 |
--------------------------------------------------------------------------------
/examples/flow_backward_warp.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 |
4 | from PIL import Image
5 |
6 | import flowpy
7 |
8 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
9 | first_image = np.asarray(Image.open("static/kitti_000010_10.png"))
10 | second_image = np.asarray(Image.open("static/kitti_000010_11.png"))
11 |
12 | flow[np.isnan(flow)] = 0
13 | warped_first_image = flowpy.backward_warp(second_image, flow)
14 |
15 | fig, axes = plt.subplots(3, 1)
16 | for ax, image, title in zip(axes, (first_image, second_image, warped_first_image),
17 | ("First Image", "Second Image", "Second image warped to first image")):
18 | ax.imshow(image)
19 | ax.set_title(title)
20 | ax.set_axis_off()
21 |
22 | plt.show()
23 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | with open("README.md") as f:
4 | long_description = f.read()
5 |
6 | setup(name='flowpy',
7 | version='0.6.0',
8 | description='Tools for working with optical flow',
9 | long_description=long_description,
10 | long_description_content_type='text/markdown',
11 | url='https://gitlab-research.centralesupelec.fr/2018seznecm/flowpy',
12 | author='Mickaël Seznec',
13 | author_email='mickael.seznec@centralesupelec.fr',
14 | license='MIT',
15 | packages=['flowpy'],
16 | install_requires=[
17 | 'matplotlib',
18 | 'numpy',
19 | 'pypng',
20 | 'scipy',
21 | ],
22 | test_requires=[
23 | 'PIL',
24 | ],
25 | scripts=[
26 | 'scripts/flowread'
27 | ],
28 | zip_safe=False)
29 |
--------------------------------------------------------------------------------
/flowpy/__init__.py:
--------------------------------------------------------------------------------
1 | """ flowpy
2 |
3 | flowpy
4 | ======
5 |
6 | Contains several utilities for working with optical flows:
7 | 1. flow_read and flow_write let you manipulate flows in .flo and .png format
8 | 2. flow_to_rgb generates a RGB representation of a flow
9 | 3. attach_arrows, attach_coord, attach_calibration_pattern provide helper functions to generate beautiful graphs with matplotlib.
10 | 4. Warp an image according to a flow, in the direct and reverse order.
11 |
12 | The library handles flow in the HWF format, a numpy.ndarray with 3 dimensions of size [H, W, 2] that hold respectively the height, width and 2d displacement in the (x, y) order.
13 |
14 | Undefined flow is attributed a NaN value.
15 | """
16 |
17 | from .flowpy import (flow_to_rgb, make_colorwheel, calibration_pattern,
18 | attach_arrows, attach_coord, attach_calibration_pattern,
19 | get_flow_max_radius, backward_warp, forward_warp, flowshow)
20 | from .flow_io import flow_read, flow_write
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Univ. Paris-Saclay, CNRS, CentraleSupelec,
4 | Thales Research & Technology
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | .hypothesis/
51 | .pytest_cache/
52 |
53 | # Translations
54 | *.mo
55 | *.pot
56 |
57 | # Django stuff:
58 | *.log
59 | local_settings.py
60 | db.sqlite3
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # IPython
79 | profile_default/
80 | ipython_config.py
81 |
82 | # pyenv
83 | .python-version
84 |
85 | # celery beat schedule file
86 | celerybeat-schedule
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | env.bak/
98 | venv.bak/
99 |
100 | # Spyder project settings
101 | .spyderproject
102 | .spyproject
103 |
104 | # Rope project settings
105 | .ropeproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 | .dmypy.json
113 | dmypy.json
114 |
115 | # Pyre type checker
116 | .pyre/
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # flowpy 💾 - A python package for working with optical flows
2 |
3 | Optical flow is the displacement map of pixels between two frames. It is a low-level analysis used in many computer vision programs.
4 |
5 | Working with optical flow may be cumbersome:
6 | - It is quite hard to represent it in a comprehensible manner.
7 | - Multiple formats exist for storing it.
8 |
9 | Flowpy provides tools to work with optical flow more easily in python.
10 |
11 | ## Installing
12 |
13 | We recommend using pip:
14 | ```bash
15 | pip install flowpy
16 | ```
17 |
18 | ## Features
19 |
20 | The main features of flowpy are:
21 | - Reading and writing optical flows in two formats:
22 | - **.flo** (as defined [here](http://vision.middlebury.edu/flow/))
23 | - **.png** (as defined [here](http://www.cvlibs.net/datasets/kitti/eval_scene_flow.php?benchmark=flow))
24 | - Visualizing optical flows with matplotlib
25 | - Backward and forward warp
26 |
27 | ## Examples
28 |
29 | ### A simple RGB plot
30 |
31 | This is the simplest example of how to use flowpy, it:
32 | - Reads a file using *flowpy.flow_read*.
33 | - Transforms the flow as an rgb image with *flowpy.flow_to_rgb* and shows it with matplotlib
34 |
35 | #### Code:
36 | ```python
37 | import flowpy
38 | import matplotlib.pyplot as plt
39 |
40 | flow = flowpy.flow_read("tests/data/kitti_occ_000010_10.flo")
41 |
42 | fig, ax = plt.subplots()
43 | ax.imshow(flowpy.flow_to_rgb(flow))
44 | plt.show()
45 | ```
46 |
47 | #### Result:
48 | ![simple_example]
49 |
50 | *Sample image from the [KITTI](http://www.cvlibs.net/datasets/kitti/eval_scene_flow.php?benchmark=flow) dataset*
51 |
52 | ### Plotting arrows, showing flow values and a calibration pattern
53 |
54 | Flowpy comes with more than just RGB plots, the main features here are:
55 | - Arrows to quickly visualize the flow
56 | - The flow values below cursor showing in the tooltips
57 | - A calibration pattern side by side as a legend for your graph
58 |
59 | #### Code:
60 | ```python
61 | flow = flowpy.flow_read("tests/data/Dimetrodon.flo")
62 | height, width, _ = flow.shape
63 |
64 | image_ratio = height / width
65 | max_radius = flowpy.get_flow_max_radius(flow)
66 |
67 | fig, (ax_1, ax_2) = plt.subplots(
68 | 1, 2, gridspec_kw={"width_ratios": [1, image_ratio]}
69 | )
70 |
71 | ax_1.imshow(flowpy.flow_to_rgb(flow))
72 | flowpy.attach_arrows(ax_1, flow)
73 | flowpy.attach_coord(ax_1, flow)
74 |
75 | flowpy.attach_calibration_pattern(ax_2, flow_max_radius=max_radius)
76 |
77 | plt.show()
78 | ```
79 |
80 | #### Result:
81 | ![complex_example]
82 |
83 | *Sample image from the [Middlebury](http://vision.middlebury.edu/flow/data/) dataset*
84 |
85 | ### Warping images (backward):
86 | If you know the flow (first_image -> second_image), you can backward warp the second_image back to first_image.
87 |
88 | ```python
89 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
90 | first_image = np.asarray(Image.open("static/kitti_000010_10.png"))
91 | second_image = np.asarray(Image.open("static/kitti_000010_11.png"))
92 |
93 | flow[np.isnan(flow)] = 0
94 | warped_first_image = flowpy.backward_warp(second_image, flow)
95 |
96 | fig, axes = plt.subplots(3, 1)
97 | for ax, image, title in zip(axes, (first_image, second_image, warped_first_image),
98 | ("First Image", "Second Image", "Second image warped to first image")):
99 | ax.imshow(image)
100 | ax.set_title(title)
101 | ax.set_axis_off()
102 |
103 | plt.show()
104 | ```
105 |
106 | #### Result:
107 | ![backward_warp_example]
108 |
109 | Note that the artifacts in the warp are normal, they are caused by unknown flows and occlusions.
110 |
111 | ### Warping images (forward):
112 |
113 | Forward warp is often less used as it is quite more complex. It relies on a k-nearest neighbor search instead of direct bi-linear interpolation.
114 |
115 | `forward_warp` is about 10x slower than `backward_warp` but you still may find it useful.
116 |
117 | ```python
118 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
119 | first_image = np.asarray(Image.open("static/kitti_000010_10.png"))
120 | second_image = np.asarray(Image.open("static/kitti_000010_11.png"))
121 |
122 | flow[np.isnan(flow)] = 0
123 | warped_second_image = flowpy.forward_warp(first_image, flow)
124 |
125 | fig, ax = plt.subplots()
126 |
127 | ax.imshow(warped_second_image)
128 | ax.set_title( "First image warped to the second")
129 | ax.set_axis_off()
130 |
131 | plt.show()
132 | ```
133 |
134 | #### Result:
135 | ![forward_warp_example]
136 |
137 |
138 | ### More
139 |
140 | You can find the above examples in the `examples` folder. You can also look in `tests`.
141 | If you encounter a bug or have an idea for a new feature, feel free to open an issue.
142 |
143 | Most of the visualization and io handling has been translated from matlab and c code from the [Middlebury flow code](http://vision.middlebury.edu/flow/code/flow-code/).
144 | Credits to thank Simon Baker, Daniel Scharste, J. P. Lewis, Stefan Roth, Michael J. Black and Richard Szeliski.
145 |
146 | [simple_example]: https://raw.githubusercontent.com/mickaelseznec/flowpy/master/static/example_rgb.png "Displaying an optical flow as an RGB image"
147 | [complex_example]: https://raw.githubusercontent.com/mickaelseznec/flowpy/master/static/example_arrows.png "Displaying an optical flow as an RGB image with arrows, tooltip and legend"
148 | [backward_warp_example]: https://raw.githubusercontent.com/mickaelseznec/flowpy/master/static/example_backward_warp.png "An example of backward warp"
149 | [forward_warp_example]: https://raw.githubusercontent.com/mickaelseznec/flowpy/master/static/example_forward_warp.png "An example of forward warp"
150 |
--------------------------------------------------------------------------------
/flowpy/flow_io.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import png
3 | import struct
4 |
5 | from pathlib import Path
6 | from warnings import warn
7 |
8 |
9 | def flow_write(output_file, flow, format=None):
10 | """
11 | Writes optical flow to file.
12 |
13 | Parameters
14 | ----------
15 | output_file: {str, pathlib.Path, file}
16 | Path of the file to write or file object.
17 | flow: numpy.ndarray
18 | 3D flow in the HWF (Height, Width, Flow) layout.
19 | flow[..., 0] should be the x-displacement
20 | flow[..., 1] should be the y-displacement
21 | format: str, optional
22 | Specify in what format the flow is written, accepted formats: "png" or "flo"
23 | If None, it is guessed on the file extension
24 |
25 | See Also
26 | --------
27 | flow_read
28 |
29 | """
30 |
31 | supported_formats = ("png", "flo")
32 |
33 | output_format = guess_extension(output_file, override=format)
34 |
35 | with FileManager(output_file, "wb") as f:
36 | if output_format == "png":
37 | flow_write_png(f, flow)
38 | else:
39 | flow_write_flo(f, flow)
40 |
41 |
42 | def flow_read(input_file, format=None):
43 | """
44 | Reads optical flow from file
45 |
46 | Parameters
47 | ----------
48 | output_file: {str, pathlib.Path, file}
49 | Path of the file to read or file object.
50 | format: str, optional
51 | Specify in what format the flow is raed, accepted formats: "png" or "flo"
52 | If None, it is guess on the file extension
53 |
54 | Returns
55 | -------
56 | flow: numpy.ndarray
57 | 3D flow in the HWF (Height, Width, Flow) layout.
58 | flow[..., 0] is the x-displacement
59 | flow[..., 1] is the y-displacement
60 |
61 | Notes
62 | -----
63 |
64 | The flo format is dedicated to optical flow and was first used in Middlebury optical flow database.
65 | The original defition can be found here: http://vision.middlebury.edu/flow/code/flow-code/flowIO.cpp
66 |
67 | The png format uses 16-bit RGB png to store optical flows.
68 | It was developped along with the KITTI Vision Benchmark Suite.
69 | More information can be found here: http://www.cvlibs.net/datasets/kitti/eval_scene_flow.php?benchmark=flow
70 |
71 | The both handle flow with invalid ``invalid'' values, to deal with occlusion for example.
72 | We convert such invalid values to NaN.
73 |
74 | See Also
75 | --------
76 | flow_write
77 |
78 | """
79 |
80 | input_format = guess_extension(input_file, override=format)
81 |
82 | with FileManager(input_file, "rb") as f:
83 | if input_format == "png":
84 | output = flow_read_png(f)
85 | else:
86 | output = flow_read_flo(f)
87 |
88 | return output
89 |
90 |
91 | def flow_read_flo(f):
92 | if (f.read(4) != b'PIEH'):
93 | warn("{} does not have a .flo file signature".format(f.name))
94 |
95 | width, height = struct.unpack("II", f.read(8))
96 | result = np.fromfile(f, dtype="float32").reshape((height, width, 2))
97 |
98 | # Set invalid flows to NaN
99 | mask_u = np.greater(np.abs(result[..., 0]), 1e9, where=(~np.isnan(result[..., 0])))
100 | mask_v = np.greater(np.abs(result[..., 1]), 1e9, where=(~np.isnan(result[..., 1])))
101 |
102 | result[mask_u | mask_v] = np.NaN
103 |
104 | return result
105 |
106 |
107 | def flow_write_flo(f, flow):
108 | SENTINEL = 1666666800.0 # Only here to look like Middlebury original files
109 | height, width, _ = flow.shape
110 |
111 | image = flow.copy()
112 | image[np.isnan(image)] = SENTINEL
113 |
114 | f.write(b'PIEH')
115 | f.write(struct.pack("II", width, height))
116 | image.astype(np.float32).tofile(f)
117 |
118 |
119 | def flow_read_png(f):
120 | width, height, stream, *_ = png.Reader(f).read()
121 |
122 | file_content = np.concatenate(list(stream)).reshape((height, width, 3))
123 | flow, valid = file_content[..., 0:2], file_content[..., 2]
124 |
125 | flow = (flow.astype(np.float) - 2 ** 15) / 64.
126 |
127 | flow[~valid.astype(np.bool)] = np.NaN
128 |
129 | return flow
130 |
131 |
132 | def flow_write_png(f, flow):
133 | SENTINEL = 0. # Only here to look like original KITTI files
134 | height, width, _ = flow.shape
135 | flow_copy = flow.copy()
136 |
137 | valid = ~(np.isnan(flow[..., 0]) | np.isnan(flow[..., 1]))
138 | flow_copy[~valid] = SENTINEL
139 |
140 | flow_copy = (flow_copy * 64. + 2 ** 15).astype(np.uint16)
141 | image = np.dstack((flow_copy, valid))
142 |
143 | writer = png.Writer(width, height, bitdepth=16, greyscale=False)
144 | writer.write(f, image.reshape((height, 3 * width)))
145 |
146 |
147 | class FileManager:
148 | def __init__(self, abstract_file, mode):
149 | self.abstract_file = abstract_file
150 | self.opened_file = None
151 | self.mode = mode
152 |
153 | def __enter__(self):
154 | if isinstance(self.abstract_file, str):
155 | self.opened_file = open(self.abstract_file, self.mode)
156 | elif isinstance(self.abstract_file, Path):
157 | self.opened_file = self.abstract_file.open(self.mode)
158 | else:
159 | return self.abstract_file
160 |
161 | return self.opened_file
162 |
163 | def __exit__(self, exc_type, exc_value, traceback):
164 | if self.opened_file is not None:
165 | self.opened_file.close()
166 |
167 |
168 | def guess_extension(abstract_file, override=None):
169 | if override is not None:
170 | return override
171 |
172 | if isinstance(abstract_file, str):
173 | return Path(abstract_file).suffix[1:]
174 | elif isinstance(abstract_file, Path):
175 | return abstract_file.suffix[1:]
176 |
177 | return Path(abstract_file.name).suffix[1:]
178 |
--------------------------------------------------------------------------------
/tests/flow_tests.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | import filecmp
3 | import matplotlib.pyplot as plt
4 | import numpy as np
5 | import os
6 | import sys
7 | import tempfile
8 | import unittest
9 |
10 | from PIL import Image
11 |
12 | import flowpy
13 |
14 |
15 | class CalibrationPatternTestCase(unittest.TestCase):
16 | def test_calibration_bright(self):
17 | fig, ax = plt.subplots()
18 | _, flow = flowpy.calibration_pattern()
19 |
20 | flowpy.attach_calibration_pattern(
21 | ax, pixel_size=255, flow_max_radius=5, background="bright"
22 | )
23 |
24 | ax.set_title("Calibration Pattern (Bright)")
25 |
26 | plt.show()
27 |
28 | def test_calibration_with_arrows(self):
29 | pattern, flow = flowpy.calibration_pattern()
30 |
31 | fig, ax = plt.subplots()
32 | ax.imshow(pattern)
33 | flowpy.attach_arrows(ax, flow)
34 | plt.show()
35 |
36 | def test_calibration_dark(self):
37 | width = 255
38 | mid_width = width // 2
39 | pattern, _ = flowpy.calibration_pattern(width, background="dark")
40 |
41 | fig, ax = plt.subplots()
42 | ax.imshow(pattern)
43 | ax.hlines(mid_width, -.5, width-.5)
44 | ax.vlines(mid_width, -.5, width-.5)
45 | circle = plt.Circle((mid_width, mid_width), mid_width, fill=False)
46 | ax.add_artist(circle)
47 | ax.set_title("Calibration Pattern (Dark)")
48 | plt.show()
49 |
50 |
51 | class FlowInputOutput(unittest.TestCase):
52 | def test_read_write_flo(self):
53 | input_filepath = "static/Dimetrodon.flo"
54 | flow = flowpy.flow_read(input_filepath)
55 |
56 | with tempfile.NamedTemporaryFile("wb", suffix=".flo") as f:
57 | flowpy.flow_write(f, flow)
58 | self.assertTrue(filecmp.cmp(f.name, input_filepath))
59 |
60 | def test_read_write_png_occ(self):
61 | input_filepath = "static/kitti_occ_000010_10.png"
62 |
63 | flow = flowpy.flow_read(input_filepath)
64 |
65 | _, output_filename = tempfile.mkstemp(suffix=".png")
66 |
67 | try:
68 | flowpy.flow_write(output_filename, flow)
69 | new_flow = flowpy.flow_read(output_filename)
70 | np.testing.assert_equal(new_flow, flow)
71 | finally:
72 | os.remove(output_filename)
73 |
74 | def test_read_write_png_noc(self):
75 | input_filepath = "static/kitti_noc_000010_10.png"
76 |
77 | flow = flowpy.flow_read(input_filepath)
78 |
79 | _, output_filename = tempfile.mkstemp(suffix=".png")
80 |
81 | try:
82 | flowpy.flow_write(output_filename, flow)
83 | new_flow = flowpy.flow_read(output_filename)
84 | np.testing.assert_equal(new_flow, flow)
85 | finally:
86 | os.remove(output_filename)
87 |
88 |
89 | class FlowDisplay(unittest.TestCase):
90 | def test_flow_to_rgb(self):
91 | flow = flowpy.flow_read("static/Dimetrodon.flo")
92 | plt.imshow(flowpy.flow_to_rgb(flow))
93 | plt.show()
94 |
95 | def test_flow_with_arrows(self):
96 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
97 |
98 | fig, ax = plt.subplots()
99 | ax.imshow(flowpy.flow_to_rgb(flow))
100 | flowpy.attach_arrows(ax, flow, xy_steps=(20, 20), scale=1)
101 |
102 | plt.show()
103 |
104 | def test_flow_arrows_and_coord(self):
105 | flow = flowpy.flow_read("static/Dimetrodon.flo")
106 |
107 | fig, ax = plt.subplots()
108 | ax.imshow(flowpy.flow_to_rgb(flow))
109 | flowpy.attach_arrows(ax, flow)
110 | flowpy.attach_coord(ax, flow)
111 |
112 | plt.show()
113 |
114 | def test_flow_arrows_coord_and_calibration_pattern(self):
115 | flow = flowpy.flow_read("static/Dimetrodon.flo")
116 | height, width, _ = flow.shape
117 | image_ratio = height / width
118 |
119 | max_radius = flowpy.get_flow_max_radius(flow)
120 |
121 | fig, (ax_1, ax_2) = plt.subplots(1, 2,
122 | gridspec_kw={"width_ratios": [1, image_ratio]})
123 |
124 | ax_1.imshow(flowpy.flow_to_rgb(flow))
125 | flowpy.attach_arrows(ax_1, flow)
126 | flowpy.attach_coord(ax_1, flow)
127 |
128 | flowpy.attach_calibration_pattern(ax_2, flow_max_radius=max_radius)
129 |
130 | plt.show()
131 |
132 |
133 | class FlowWarp(unittest.TestCase):
134 | def test_backward_warp_greyscale(self):
135 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
136 | first_image = np.asarray(Image.open("static/kitti_000010_10.png").convert("L"))
137 | second_image = np.asarray(Image.open("static/kitti_000010_11.png").convert("L"))
138 |
139 | flow[np.isnan(flow)] = 0
140 | warped_first_image = flowpy.backward_warp(second_image, flow)
141 |
142 | fig, (ax_1, ax_2, ax_3) = plt.subplots(3, 1)
143 | ax_1.imshow(first_image)
144 | ax_2.imshow(second_image)
145 | ax_3.imshow(warped_first_image)
146 |
147 | plt.show()
148 |
149 | def test_backward_warp_rgb(self):
150 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
151 | first_image = np.asarray(Image.open("static/kitti_000010_10.png"))
152 | second_image = np.asarray(Image.open("static/kitti_000010_11.png"))
153 |
154 | flow[np.isnan(flow)] = 0
155 | warped_first_image = flowpy.backward_warp(second_image, flow)
156 |
157 | fig, (ax_1, ax_2, ax_3) = plt.subplots(3, 1)
158 | ax_1.imshow(first_image)
159 | ax_2.imshow(second_image)
160 | ax_3.imshow(warped_first_image)
161 |
162 | plt.show()
163 |
164 | def test_forward_warp_rgb(self):
165 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
166 | first_image = np.asarray(Image.open("static/kitti_000010_10.png"))
167 | second_image = np.asarray(Image.open("static/kitti_000010_11.png"))
168 |
169 | flow[np.isnan(flow)] = 0
170 | warped_second_image = flowpy.forward_warp(first_image, flow, k=1)
171 |
172 | fig, (ax_1, ax_2, ax_3) = plt.subplots(3, 1)
173 | ax_1.imshow(first_image)
174 | ax_2.imshow(flowpy.flow_to_rgb(flow))
175 | ax_3.imshow(warped_second_image)
176 |
177 | plt.show()
178 |
179 | def test_forward_warp_greyscale(self):
180 | flow = flowpy.flow_read("static/kitti_occ_000010_10.png")
181 | first_image = np.asarray(Image.open("static/kitti_000010_10.png").convert("L"))
182 | second_image = np.asarray(Image.open("static/kitti_000010_11.png").convert("L"))
183 |
184 | flow[np.isnan(flow)] = 0
185 | warped_second_image = flowpy.forward_warp(first_image, flow, k=4)
186 |
187 | fig, (ax_1, ax_2, ax_3) = plt.subplots(3, 1)
188 | ax_1.imshow(first_image)
189 | ax_2.imshow(flowpy.flow_to_rgb(flow))
190 | ax_3.imshow(warped_second_image)
191 |
192 | plt.show()
193 |
194 | if __name__ == "__main__":
195 | unittest.main()
196 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | flowpy API documentation
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 | flowpy
24 | flowpy
25 | Contains several utilities for working with optical flows:
26 | 1. flow_read and flow_write let you manipulate flows in .flo and .png format
27 | 2. flow_to_rgb generates a RGB representation of a flow
28 | 3. attach_arrows, attach_coord, attach_calibration_pattern provide helper functions to generate beautiful graphs with matplotlib.
29 | The library handles flow in the HWF format, a numpy.ndarray with 3 dimensions of size [H, W, 2] that hold respectively the height, width and 2d displacement in the (x, y) order.
30 | Undefined flow is attributed a NaN value.
31 |
32 |
33 | Expand source code
34 |
35 | """ flowpy
36 |
37 | flowpy
38 | ======
39 |
40 | Contains several utilities for working with optical flows:
41 | 1. flow_read and flow_write let you manipulate flows in .flo and .png format
42 | 2. flow_to_rgb generates a RGB representation of a flow
43 | 3. attach_arrows, attach_coord, attach_calibration_pattern provide helper functions to generate beautiful graphs with matplotlib.
44 |
45 | The library handles flow in the HWF format, a numpy.ndarray with 3 dimensions of size [H, W, 2] that hold respectively the height, width and 2d displacement in the (x, y) order.
46 |
47 | Undefined flow is attributed a NaN value.
48 | """
49 |
50 | from .flowpy import (flow_to_rgb, make_colorwheel, calibration_pattern,
51 | attach_arrows, attach_coord, attach_calibration_pattern,
52 | get_flow_max_radius)
53 | from .flow_io import flow_read, flow_write
54 |
55 |
56 |
69 |
71 |
73 |
75 |
76 |
92 |
93 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/scripts/flowread:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | import argparse
4 | import flowpy
5 | import matplotlib
6 | import tifffile
7 | import numpy as np
8 | import sys
9 | from tqdm import tqdm
10 | matplotlib.use('Qt5Agg')
11 |
12 | from matplotlib.backends.backend_qt5agg import (
13 | FigureCanvasQTAgg as FigureCanvas,
14 | NavigationToolbar2QT as NavigationToolbar
15 | )
16 | from matplotlib.figure import Figure
17 | from matplotlib.widgets import Slider
18 | from PyQt5 import QtCore, QtWidgets
19 | from pathlib import Path
20 |
21 | try:
22 | import ffmpeg
23 | except ImportError:
24 | ffmpeg = None
25 |
26 |
27 | def main():
28 | parser = argparse.ArgumentParser()
29 | parser.add_argument("file_paths", nargs="*")
30 | args = parser.parse_args()
31 |
32 | qt_app = QtWidgets.QApplication([""])
33 |
34 | main_window = DOFQTWindow()
35 | if args.file_paths:
36 | main_window.set_flow_source(args.file_paths)
37 |
38 | main_window.show()
39 | sys.exit(qt_app.exec_())
40 |
41 |
42 | class DOFQTWindow(QtWidgets.QMainWindow):
43 | flowSourceChanged = QtCore.pyqtSignal(list, name="flowSourceChanged")
44 | exportRequested = QtCore.pyqtSignal(str, name="exportRequested")
45 |
46 | def __init__(self):
47 | QtWidgets.QMainWindow.__init__(self)
48 |
49 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
50 | self.setWindowTitle("DOFReader")
51 |
52 | self.main_widget = DOFMainWidget(parent=self)
53 | self.main_widget.setFocus()
54 | self.setCentralWidget(self.main_widget)
55 |
56 | menu_bar = self.menuBar()
57 | file_menu = menu_bar.addMenu("&File")
58 | file_menu.addAction("&Export flow...", self.export_flow_dialog)
59 |
60 | def set_flow_source(self, file_path):
61 | self.flowSourceChanged.emit(file_path)
62 |
63 | def export_flow_dialog(self):
64 | filename, _ = QtWidgets.QFileDialog.getSaveFileName(
65 | self,
66 | "Choose export path",
67 | "",
68 | ""
69 | )
70 | self.exportRequested.emit(filename)
71 |
72 |
73 | class DOFMainWidget(QtWidgets.QWidget):
74 | flowScaleChanged = QtCore.pyqtSignal(float, name="flowScaleChanged")
75 | frameChanged = QtCore.pyqtSignal(np.ndarray, name="frameChanged")
76 |
77 | def __init__(self, parent=None):
78 | super(QtWidgets.QWidget, self).__init__(parent)
79 |
80 | self.flow_sequence = None
81 |
82 | self.plot_options = PlotOptions(self.flowScaleChanged, parent=self)
83 | self.canvas = MatplotlibCanvas(self.plot_options.getValue(), parent=self)
84 | self.toolbar = NavigationToolbar(self.canvas, self)
85 | self.slider_box = FrameSliderBox(self)
86 |
87 | self.layout = QtWidgets.QVBoxLayout(self)
88 | self.layout.addWidget(self.toolbar)
89 | self.layout.addWidget(self.canvas)
90 | self.layout.addWidget(self.slider_box)
91 | self.layout.addWidget(self.plot_options)
92 |
93 | self.plot_options.valueChanged.connect(self.canvas.handle_plot_options_changed)
94 | self.slider_box.slider.valueChanged.connect(self.handle_cursor_changed)
95 | self.parent().flowSourceChanged.connect(self.handle_flow_source_changed)
96 | self.parent().exportRequested.connect(self.handle_export_requested)
97 |
98 | def handle_flow_source_changed(self, path):
99 | self.flow_sequence = FlowOpener.open(path)
100 | self.slider_box.slider.setValue(1)
101 | self.slider_box.slider.setMaximum(self.flow_sequence.shape[0])
102 | self.emit_frame_changed(self.flow_sequence[0])
103 |
104 | def handle_cursor_changed(self, cursor_value):
105 | self.slider_box.slider_label.setText(str(cursor_value))
106 | self.emit_frame_changed(self.flow_sequence[cursor_value - 1])
107 |
108 | def handle_export_requested(self, filename):
109 | if ffmpeg is None:
110 | print("Export aborted, ffmpeg-python not found")
111 | return
112 |
113 | print("Exporting frames to " + filename)
114 |
115 | length, height, width, _ = self.flow_sequence.shape
116 |
117 | ffmpeg_process = (
118 | ffmpeg
119 | .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width, height))
120 | .output(filename, pix_fmt='yuv420p')
121 | .overwrite_output()
122 | .run_async(pipe_stdin=True)
123 | )
124 |
125 | plot_parameters = self.plot_options.getValue()
126 | flowpy_options = MatplotlibCanvas.get_flowpy_options(plot_parameters)
127 |
128 | for frame in tqdm(self.flow_sequence):
129 | if plot_parameters["auto_scale"]:
130 | flowpy_options["flow_max_radius"] = flowpy.get_flow_max_radius(frame)
131 | rendered_flow = flowpy.flow_to_rgb(frame, **flowpy_options)
132 | ffmpeg_process.stdin.write(rendered_flow.astype(np.uint8).tobytes())
133 |
134 | ffmpeg_process.stdin.close()
135 | ffmpeg_process.wait()
136 |
137 | print("Export finished")
138 |
139 | def emit_frame_changed(self, flow):
140 | auto_scale_radius = flowpy.get_flow_max_radius(flow)
141 | self.plot_options.setAutoScaleRadius(auto_scale_radius)
142 | self.frameChanged.emit(flow)
143 |
144 |
145 | class MatplotlibCanvas(FigureCanvas):
146 | def __init__(self, plot_options, parent=None):
147 | fig = Figure(figsize=(5, 4), dpi=100)
148 |
149 | FigureCanvas.__init__(self, fig)
150 | FigureCanvas.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
151 | FigureCanvas.updateGeometry(self)
152 | self.fig = fig
153 | self.setParent(parent)
154 |
155 | self.ax_im, self.ax_cal = fig.subplots(1, 2)
156 | self.flow_image = None
157 | self.arrows = None
158 | self.circle = None
159 |
160 | self.parent().frameChanged.connect(self.handle_flow_changed)
161 |
162 | def clean_canvas(self):
163 | if self.flow_image:
164 | self.flow_image.remove()
165 | self.flow_image = None
166 |
167 | if self.arrows:
168 | self.arrows.remove()
169 | self.arrows = None
170 |
171 | if self.circle:
172 | self.circle.remove()
173 | self.circle = None
174 |
175 | @staticmethod
176 | def get_flowpy_options(plot_options):
177 | flowpy_options = {}
178 |
179 | flowpy_options["background"] = plot_options["background"]
180 | if plot_options["auto_scale"] and plot_options["auto_scale_radius"] > 0:
181 | flowpy_options["flow_max_radius"] = plot_options["auto_scale_radius"]
182 | else:
183 | flowpy_options["flow_max_radius"] = plot_options["max_radius"]
184 |
185 | return flowpy_options
186 |
187 | def update_rendered_flow(self):
188 | self.clean_canvas()
189 |
190 | plot_options = self.parent().plot_options.getValue()
191 | height, width, _ = self.flow.shape
192 |
193 | grid_spec = matplotlib.gridspec.GridSpec(1, 2, width_ratios=[1, height/width])
194 | self.ax_im.set_position(grid_spec[0].get_position(self.fig))
195 | self.ax_cal.set_position(grid_spec[1].get_position(self.fig))
196 |
197 | flowpy_options = self.get_flowpy_options(plot_options)
198 | rendered_flow = flowpy.flow_to_rgb(self.flow, **flowpy_options)
199 | self.flow_image = self.ax_im.imshow(rendered_flow)
200 |
201 | if plot_options["show_arrows"]:
202 | self.arrows = flowpy.attach_arrows(self.ax_im, self.flow, scale_units="xy", scale=1.0)
203 | flowpy.attach_coord(self.ax_im, self.flow)
204 | _, self.circle = flowpy.attach_calibration_pattern(self.ax_cal, **flowpy_options)
205 |
206 | self.flow_image.axes.figure.canvas.draw()
207 |
208 | def handle_flow_changed(self, flow):
209 | self.flow = flow
210 | self.update_rendered_flow()
211 |
212 | def handle_plot_options_changed(self, _):
213 | self.update_rendered_flow()
214 |
215 |
216 | class FrameSliderBox(QtWidgets.QGroupBox):
217 | def __init__(self, parent=None):
218 | super(FrameSliderBox, self).__init__(parent)
219 | self.setTitle("Frame index")
220 |
221 | main_layout = QtWidgets.QHBoxLayout()
222 |
223 | self.slider_label = QtWidgets.QLabel(str(1))
224 | self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, parent)
225 | self.slider.setTickPosition(QtWidgets.QSlider.TicksAbove)
226 | self.slider.setMinimum(1)
227 | self.slider.setMaximum(1)
228 |
229 | main_layout.addWidget(self.slider_label)
230 | main_layout.addWidget(self.slider)
231 | self.setLayout(main_layout)
232 |
233 |
234 | class PlotOptions(QtWidgets.QGroupBox):
235 | valueChanged = QtCore.pyqtSignal(dict, name="valueChanged")
236 | default_parameters = {
237 | "background": "bright",
238 | "show_arrows": False,
239 | "auto_scale": True,
240 | "max_radius": 0.0,
241 | "auto_scale_radius": 0.0,
242 | }
243 |
244 | def __init__(self, flowScaleChanged, parent=None):
245 | super().__init__("Plot parameters", parent)
246 | self.parameters = self.default_parameters.copy()
247 |
248 | main_layout = QtWidgets.QHBoxLayout()
249 |
250 | self.background = QtWidgets.QCheckBox("Black background", self)
251 | self.background.setChecked(self.default_parameters["background"] == "dark")
252 | self.background.toggled.connect(self.handle_state_changed)
253 |
254 | self.arrows = QtWidgets.QCheckBox("arrows", self)
255 | self.arrows.setChecked(self.default_parameters["show_arrows"])
256 | self.arrows.toggled.connect(self.handle_state_changed)
257 |
258 | self.auto_scale = QtWidgets.QCheckBox("auto scale", self)
259 | self.auto_scale.setChecked(not self.default_parameters["max_radius"])
260 | self.auto_scale.toggled.connect(self.handle_state_changed)
261 |
262 | self.max_radius = QtWidgets.QDoubleSpinBox(self)
263 | self.max_radius.setEnabled(False)
264 | self.max_radius.setValue(0.1)
265 | self.max_radius.setRange(0.1, 1e3)
266 | self.max_radius.setSingleStep(0.1)
267 | self.max_radius.valueChanged.connect(self.handle_state_changed)
268 | flowScaleChanged.connect(self.handle_flow_scale_changed)
269 |
270 | main_layout.addWidget(self.background)
271 | main_layout.addWidget(self.arrows)
272 | main_layout.addWidget(self.auto_scale)
273 | main_layout.addWidget(self.max_radius)
274 |
275 | self.setLayout(main_layout)
276 | self.handle_state_changed()
277 |
278 | def synchronize_parameters(self):
279 | self.parameters["background"] = "dark" if self.background.isChecked() else "bright"
280 | self.parameters["show_arrows"] = self.arrows.isChecked()
281 | self.parameters["auto_scale"] = self.auto_scale.isChecked()
282 | self.parameters["max_radius"] = self.max_radius.value()
283 |
284 | def setAutoScaleRadius(self, value):
285 | self.parameters["auto_scale_radius"] = value
286 | if self.parameters["auto_scale"]:
287 | self.max_radius.valueChanged.disconnect()
288 | self.max_radius.setValue(value)
289 | self.max_radius.valueChanged.connect(self.handle_state_changed)
290 |
291 | def handle_state_changed(self):
292 | self.synchronize_parameters()
293 | self.max_radius.setEnabled(not self.parameters["auto_scale"])
294 |
295 | self.valueChanged.emit(self.getValue())
296 |
297 | def handle_flow_scale_changed(self, value):
298 | self.max_radius.setValue(value)
299 |
300 | def getValue(self):
301 | return self.parameters
302 |
303 |
304 | class FlowOpener():
305 | @staticmethod
306 | def open(input_paths):
307 | if Path(input_paths[0]).suffix.lower() in [".tiff", ".tif"]:
308 | assert len(input_paths) == 2, "Must provide two tiff files"
309 |
310 | data = tifffile.imread(input_paths)
311 | if data.ndim == 3:
312 | data = np.asarray([data])
313 | data = data.transpose((1, 2, 3, 0))
314 | else:
315 | data = np.stack([flowpy.flow_read(path) for path in input_paths])
316 |
317 | return data
318 |
319 |
320 | if __name__ == "__main__":
321 | main()
322 |
--------------------------------------------------------------------------------
/flowpy/flowpy.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import matplotlib.pyplot as plt
3 | import numpy as np
4 |
5 | from collections import namedtuple
6 | from itertools import accumulate
7 | from matplotlib.ticker import AutoMinorLocator
8 | from scipy.ndimage import map_coordinates
9 | from scipy.spatial import cKDTree
10 |
11 | DEFAULT_TRANSITIONS = (15, 6, 4, 11, 13, 6)
12 |
13 |
14 | def flow_to_rgb(flow, flow_max_radius=None, background="bright", custom_colorwheel=None):
15 | """
16 | Creates a RGB representation of an optical flow.
17 |
18 | Parameters
19 | ----------
20 | flow: numpy.ndarray
21 | 3D flow in the HWF (Height, Width, Flow) layout.
22 | flow[..., 0] should be the x-displacement
23 | flow[..., 1] should be the y-displacement
24 |
25 | flow_max_radius: float, optionnal
26 | Set the radius that gives the maximum color intensity, useful for comparing different flows.
27 | Default: The normalization is based on the input flow maximum radius.
28 |
29 | background: str, optionnal
30 | States if zero-valued flow should look 'bright' or 'dark'
31 | Default: "bright"
32 |
33 | custom_colorwheel: numpy.ndarray
34 | Use a custom colorwheel for specific hue transition lengths.
35 | By default, the default transition lengths are used.
36 |
37 | Returns
38 | -------
39 | rgb_image: numpy.ndarray
40 | A 2D RGB image that represents the flow
41 |
42 | See Also
43 | --------
44 | make_colorwheel
45 |
46 | """
47 |
48 | valid_backgrounds = ("bright", "dark")
49 | if background not in valid_backgrounds:
50 | raise ValueError("background should be one the following: {}, not {}".format(
51 | valid_backgrounds, background))
52 |
53 | wheel = make_colorwheel() if custom_colorwheel is None else custom_colorwheel
54 |
55 | flow_height, flow_width, _ = flow.shape
56 |
57 | complex_flow = flow[..., 0] + 1j * flow[..., 1]
58 | complex_flow, nan_mask = replace_nans(complex_flow)
59 |
60 | radius, angle = np.abs(complex_flow), np.angle(complex_flow)
61 |
62 | if flow_max_radius is None:
63 | flow_max_radius = np.max(radius)
64 |
65 | if flow_max_radius > 0:
66 | radius /= flow_max_radius
67 |
68 | ncols = len(wheel)
69 |
70 | # Map the angles from (-pi, pi] to [0, 2pi) to [0, ncols - 1)
71 | angle[angle < 0] += 2 * np.pi
72 | angle = angle * ((ncols - 1) / (2 * np.pi))
73 |
74 | # Make the wheel cyclic for interpolation
75 | wheel = np.vstack((wheel, wheel[0]))
76 |
77 | # Interpolate the hues
78 | (angle_fractional, angle_floor), angle_ceil = np.modf(angle), np.ceil(angle)
79 | angle_fractional = angle_fractional.reshape((angle_fractional.shape) + (1,))
80 | float_hue = (wheel[angle_floor.astype(np.int)] * (1 - angle_fractional) +
81 | wheel[angle_ceil.astype(np.int)] * angle_fractional)
82 |
83 | ColorizationArgs = namedtuple("ColorizationArgs", [
84 | 'move_hue_valid_radius',
85 | 'move_hue_oversized_radius',
86 | 'invalid_color'])
87 |
88 | def move_hue_on_V_axis(hues, factors):
89 | return hues * np.expand_dims(factors, -1)
90 |
91 | def move_hue_on_S_axis(hues, factors):
92 | return 255. - np.expand_dims(factors, -1) * (255. - hues)
93 |
94 | if background == "dark":
95 | parameters = ColorizationArgs(move_hue_on_V_axis, move_hue_on_S_axis,
96 | np.array([255, 255, 255], dtype=np.float))
97 | else:
98 | parameters = ColorizationArgs(move_hue_on_S_axis, move_hue_on_V_axis,
99 | np.array([0, 0, 0], dtype=np.float))
100 |
101 | colors = parameters.move_hue_valid_radius(float_hue, radius)
102 |
103 | oversized_radius_mask = radius > 1
104 | colors[oversized_radius_mask] = parameters.move_hue_oversized_radius(
105 | float_hue[oversized_radius_mask],
106 | 1 / radius[oversized_radius_mask]
107 | )
108 | colors[nan_mask] = parameters.invalid_color
109 |
110 | return colors.astype(np.uint8)
111 |
112 |
113 | @functools.lru_cache(maxsize=2)
114 | def make_colorwheel(transitions=DEFAULT_TRANSITIONS):
115 | """
116 | Creates a color wheel.
117 |
118 | A color wheel defines the transitions between the six primary hues:
119 | Red(255, 0, 0), Yellow(255, 255, 0), Green(0, 255, 0), Cyan(0, 255, 255), Blue(0, 0, 255) and Magenta(255, 0, 255).
120 |
121 | Parameters
122 | ----------
123 | transitions: sequence_like
124 | Contains the length of the six transitions.
125 | Defaults to (15, 6, 4, 11, 13, 6), based on humain perception.
126 |
127 | Returns
128 | -------
129 | colorwheel: numpy.ndarray
130 | The RGB values of the transitions in the color space.
131 |
132 | Notes
133 | -----
134 | For more information, take a look at
135 | https://web.archive.org/web/20051107102013/http://members.shaw.ca/quadibloc/other/colint.htm
136 |
137 | """
138 |
139 | colorwheel_length = sum(transitions)
140 |
141 | # The red hue is repeated to make the color wheel cyclic
142 | base_hues = map(np.array,
143 | ([255, 0, 0], [255, 255, 0], [0, 255, 0],
144 | [0, 255, 255], [0, 0, 255], [255, 0, 255],
145 | [255, 0, 0]))
146 |
147 | colorwheel = np.zeros((colorwheel_length, 3), dtype="uint8")
148 | hue_from = next(base_hues)
149 | start_index = 0
150 | for hue_to, end_index in zip(base_hues, accumulate(transitions)):
151 | transition_length = end_index - start_index
152 |
153 | colorwheel[start_index:end_index] = np.linspace(
154 | hue_from, hue_to, transition_length, endpoint=False)
155 | hue_from = hue_to
156 | start_index = end_index
157 |
158 | return colorwheel
159 |
160 |
161 | def calibration_pattern(pixel_size=151, flow_max_radius=1, **flow_to_rgb_args):
162 | """
163 | Generates a calibration pattern.
164 |
165 | Useful to add a legend to your optical flow plots.
166 |
167 | Parameters
168 | ----------
169 | pixel_size: int
170 | Radius of the square test pattern.
171 | flow_max_radius: float
172 | The maximum radius value represented by the calibration pattern.
173 | flow_to_rgb_args: kwargs
174 | Arguments passed to the flow_to_rgb function
175 |
176 | Returns
177 | -------
178 | calibration_img: numpy.ndarray
179 | The RGB image representation of the calibration pattern.
180 | calibration_flow: numpy.ndarray
181 | The flow represented in the calibration_pattern. In HWF layout
182 |
183 | """
184 | half_width = pixel_size // 2
185 |
186 | y_grid, x_grid = np.mgrid[:pixel_size, :pixel_size]
187 |
188 | u = flow_max_radius * (x_grid / half_width - 1)
189 | v = flow_max_radius * (y_grid / half_width - 1)
190 |
191 | flow = np.zeros((pixel_size, pixel_size, 2))
192 | flow[..., 0] = u
193 | flow[..., 1] = v
194 |
195 | flow_to_rgb_args["flow_max_radius"] = flow_max_radius
196 | img = flow_to_rgb(flow, **flow_to_rgb_args)
197 |
198 | return img, flow
199 |
200 |
201 | def attach_arrows(ax, flow, xy_steps=(20, 20),
202 | units="xy", color="w", angles="xy", **quiver_kwargs):
203 | """
204 | Attach the flow arrows to a matplotlib axes using quiver.
205 |
206 | Parameters:
207 | -----------
208 | ax: matplotlib.axes
209 | The axes the arrows should be plotted on.
210 | flow: numpy.ndarray
211 | 3D flow in the HWF (Height, Width, Flow) layout.
212 | flow[..., 0] should be the x-displacement
213 | flow[..., 1] should be the y-displacement
214 | xy_steps: sequence_like
215 | The arrows are plotted every xy_steps[0] in the x-dimension and xy_steps[1] in the y-dimension
216 |
217 | Quiver Parameters:
218 | ------------------
219 | The following parameters are here to override matplotlib.quiver's defaults.
220 | units: str
221 | See matplotlib.quiver documentation.
222 | color: str
223 | See matplotlib.quiver documentation.
224 | angles: str
225 | See matplotlib.quiver documentation.
226 | quiver_kwargs: kwargs
227 | Other parameters passed to matplotlib.quiver
228 | See matplotlib.quiver documentation.
229 |
230 | Returns
231 | -------
232 | quiver_artist: matplotlib.artist
233 | See matplotlib.quiver documentation
234 | Useful for removing the arrows from the figure
235 |
236 | """
237 | height, width, _ = flow.shape
238 |
239 | y_grid, x_grid = np.mgrid[:height, :width]
240 |
241 | step_x, step_y = xy_steps
242 | half_step_x, half_step_y = step_x // 2, step_y // 2
243 |
244 | return ax.quiver(
245 | x_grid[half_step_x::step_x, half_step_y::step_y],
246 | y_grid[half_step_x::step_x, half_step_y::step_y],
247 | flow[half_step_x::step_x, half_step_y::step_y, 0],
248 | flow[half_step_x::step_x, half_step_y::step_y, 1],
249 | angles=angles,
250 | units=units, color=color, **quiver_kwargs,
251 | )
252 |
253 |
254 | def attach_coord(ax, flow, extent=None):
255 | """
256 | Attach the flow value to the coordinate tooltip.
257 |
258 | It allows you to see on the same figure, the RGB value of the pixel and the underlying value of the flow.
259 | Shows cartesian and polar coordinates.
260 |
261 | Parameters:
262 | -----------
263 | ax: matplotlib.axes
264 | The axes the arrows should be plotted on.
265 | flow: numpy.ndarray
266 | 3D flow in the HWF (Height, Width, Flow) layout.
267 | flow[..., 0] should be the x-displacement
268 | flow[..., 1] should be the y-displacement
269 | extent: sequence_like, optional
270 | Use this parameters in combination with matplotlib.imshow to resize the RGB plot.
271 | See matplotlib.imshow extent parameter.
272 | See attach_calibration_pattern
273 |
274 | """
275 | height, width, _ = flow.shape
276 | base_format = ax.format_coord
277 | if extent is not None:
278 | left, right, bottom, top = extent
279 | x_ratio = width / (right - left)
280 | y_ratio = height / (top - bottom)
281 |
282 | def new_format_coord(x, y):
283 | if extent is None:
284 | int_x = int(x + 0.5)
285 | int_y = int(y + 0.5)
286 | else:
287 | int_x = int((x - left) * x_ratio)
288 | int_y = int((y - bottom) * y_ratio)
289 |
290 | if 0 <= int_x < width and 0 <= int_y < height:
291 | format_string = "Coord: x={}, y={} / Flow: ".format(int_x, int_y)
292 |
293 | u, v = flow[int_y, int_x, :]
294 | if np.isnan(u) or np.isnan(v):
295 | format_string += "invalid"
296 | else:
297 | complex_flow = u - 1j * v
298 | r, h = np.abs(complex_flow), np.angle(complex_flow, deg=True)
299 | format_string += ("u={:.2f}, v={:.2f} (cartesian) ρ={:.2f}, θ={:.2f}° (polar)"
300 | .format(u, v, r, h))
301 | return format_string
302 | else:
303 | return base_format(x, y)
304 |
305 | ax.format_coord = new_format_coord
306 |
307 |
308 | def attach_calibration_pattern(ax, **calibration_pattern_kwargs):
309 | """
310 | Attach a calibration pattern to axes.
311 |
312 | This function uses calibration_pattern to generate a figure and shows it as nicely as possible.
313 |
314 | Parameters:
315 | -----------
316 | calibration_pattern_kwargs: kwargs, optional
317 | Parameters to be given to the calibration_pattern function.
318 |
319 | See Also:
320 | ---------
321 | calibration_pattern
322 |
323 | Returns
324 | -------
325 | image_axes: matplotlib.AxesImage
326 | See matplotlib.imshow documentation
327 | Useful for changing the image dynamically
328 | circle_artist: matplotlib.artist
329 | See matplotlib.circle documentation
330 | Useful for removing the circle from the figure
331 |
332 | """
333 | pattern, flow = calibration_pattern(**calibration_pattern_kwargs)
334 | flow_max_radius = calibration_pattern_kwargs.get("flow_max_radius", 1)
335 |
336 | extent = (-flow_max_radius, flow_max_radius) * 2
337 |
338 | image = ax.imshow(pattern, extent=extent)
339 | ax.spines["top"].set_visible(False)
340 | ax.spines["right"].set_visible(False)
341 |
342 | for spine in ("bottom", "left"):
343 | ax.spines[spine].set_position("zero")
344 | ax.spines[spine].set_linewidth(1)
345 |
346 | ax.xaxis.set_minor_locator(AutoMinorLocator())
347 | ax.yaxis.set_minor_locator(AutoMinorLocator())
348 |
349 | attach_coord(ax, flow, extent=extent)
350 |
351 | circle = plt.Circle((0, 0), flow_max_radius, fill=False, lw=1)
352 | ax.add_artist(circle)
353 |
354 | return image, circle
355 |
356 |
357 | def backward_warp(second_image, flow, **map_coordinates_kwargs):
358 | """
359 | Compute the backwarp warp of an image.
360 |
361 | Given second_image and the flow from first_image to second_image, it warps the second_image to something close to the first image if the flow is accurate.
362 |
363 | Parameters:
364 | -----------
365 | second_image: numpy.ndarray
366 | Image of the form [H, W] or [H, W, C] for greyscale or RGB images
367 | flow: numpy.ndarray
368 | 3D flow in the HWF (Height, Width, Flow) layout, from first_image to second_image.
369 | flow[..., 0] should be the x-displacement
370 | flow[..., 1] should be the y-displacement
371 | map_coordinates_kwargs: kwargs
372 | Keyword arguments passed to scipy.ndimage.map_coordinates
373 | Most important ones are *mode* for out-of-bound handling (defaults to nearest),
374 | and "order" to set the quality of the interpolation.
375 | see scipy.ndimage.map_coordinates
376 |
377 | Returns
378 | -------
379 | first_image: numpy.ndarray
380 | The warped image with same dimensions as second_image.
381 | """
382 | height, width, *_ = second_image.shape
383 | coord = np.mgrid[:height, :width]
384 |
385 | gx = (coord[1] + flow[..., 0])
386 | gy = (coord[0] + flow[..., 1])
387 |
388 | if "mode" not in map_coordinates_kwargs:
389 | map_coordinates_kwargs["mode"] = "nearest"
390 |
391 | first_image = np.zeros_like(second_image)
392 | if second_image.ndim == 3:
393 | for dim in range(second_image.shape[2]):
394 | map_coordinates(second_image[..., dim], (gy, gx), first_image[..., dim], **map_coordinates_kwargs)
395 | else:
396 | map_coordinates(second_image, (gy, gx), first_image, **map_coordinates_kwargs)
397 | return first_image
398 |
399 |
400 | def forward_warp(first_image, flow, k=4):
401 | """
402 | Compute the forward warp of an image.
403 |
404 | Given first_image and the flow from first_image to second_image, it warps the first_image to something close to the first image if the flow is accurate.
405 |
406 | It uses a k-nearest neighbors search to perform an interpolation.
407 |
408 | Parameters:
409 | -----------
410 | first_image: numpy.ndarray
411 | Image of the form [H, W] or [H, W, C] for greyscale or RGB images
412 | flow: numpy.ndarray
413 | 3D flow in the HWF (Height, Width, Flow) layout, from first_image to second_image.
414 | flow[..., 0] should be the x-displacement
415 | flow[..., 1] should be the y-displacement
416 | k: int, optional
417 | How many neighbors should be taken into account to interpolate.
418 |
419 | Returns
420 | -------
421 | second_image: numpy.ndarray
422 | The warped image with same dimensions as first_image.
423 | """
424 | first_image_3d = first_image[..., np.newaxis] if first_image.ndim == 2 else first_image
425 | height, width, channels = first_image_3d.shape
426 |
427 | coord = np.mgrid[:height, :width]
428 | grid = coord.transpose(1, 2, 0).reshape((width * height, 2))
429 |
430 | gx = (coord[1] + flow[..., 0])
431 | gy = (coord[0] + flow[..., 1])
432 |
433 | warped_points = np.asarray((gy.flatten(), gx.flatten())).T
434 | kdt = cKDTree(warped_points)
435 |
436 | distance, neighbor = kdt.query(grid, k=k)
437 |
438 | y, x = neighbor // width, neighbor % width
439 |
440 | neigbor_values = first_image_3d[(y, x)]
441 |
442 | if k == 1:
443 | second_image_flat = neigbor_values
444 | else:
445 | weights = np.exp(-distance[..., np.newaxis])
446 | normalizer = np.sum(weights, axis=1)
447 |
448 | second_image_flat = np.sum(neigbor_values * weights, axis=1)
449 | second_image_flat = (second_image_flat / normalizer).astype(first_image.dtype)
450 |
451 | return second_image_flat.reshape(first_image.shape)
452 |
453 |
454 | def replace_nans(array, value=0):
455 | nan_mask = np.isnan(array)
456 | array[nan_mask] = value
457 |
458 | return array, nan_mask
459 |
460 |
461 | def get_flow_max_radius(flow):
462 | return np.sqrt(np.nanmax(np.sum(flow ** 2, axis=2)))
463 |
464 |
465 | def flowshow(flow, with_calibration_pattern=True, with_arrows=True, with_tooltip=True, max_radius=None):
466 | height, width, _ = flow.shape
467 | image_ratio = height / width
468 |
469 | if max_radius is None:
470 | max_radius = get_flow_max_radius(flow)
471 |
472 | if with_calibration_pattern:
473 | fig, axes = plt.subplots(1, 2, gridspec_kw={"width_ratios": [1, image_ratio]})
474 | ax_1, ax_2 = axes
475 | else:
476 | fig, axes = plt.subplots()
477 | ax_1 = axes
478 |
479 | ax_1.imshow(flow_to_rgb(flow))
480 |
481 | if with_arrows:
482 | attach_arrows(ax_1, flow)
483 |
484 | if with_tooltip:
485 | attach_coord(ax_1, flow)
486 |
487 | if with_calibration_pattern:
488 | attach_calibration_pattern(ax_2, flow_max_radius=max_radius)
489 |
490 | return fig, axes
491 |
--------------------------------------------------------------------------------
/docs/flow_io.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | flowpy.flow_io API documentation
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Module flowpy.flow_io
21 |
22 |
23 |
24 |
25 | Expand source code
26 |
27 | import numpy as np
28 | import png
29 | import struct
30 |
31 | from pathlib import Path
32 | from warnings import warn
33 |
34 |
35 | def flow_write(output_file, flow, format=None):
36 | """
37 | Writes optical flow to file.
38 |
39 | Parameters
40 | ----------
41 | output_file: {str, pathlib.Path, file}
42 | Path of the file to write or file object.
43 | flow: numpy.ndarray
44 | 3D flow in the HWF (Height, Width, Flow) layout.
45 | flow[..., 0] should be the x-displacement
46 | flow[..., 1] should be the y-displacement
47 | format: str, optional
48 | Specify in what format the flow is written, accepted formats: "png" or "flo"
49 | If None, it is guessed on the file extension
50 |
51 | See Also
52 | --------
53 | flow_read
54 |
55 | """
56 |
57 | supported_formats = ("png", "flo")
58 |
59 | output_format = guess_extension(output_file, override=format)
60 |
61 | with FileManager(output_file, "wb") as f:
62 | if output_format == "png":
63 | flow_write_png(f, flow)
64 | else:
65 | flow_write_flo(f, flow)
66 |
67 |
68 | def flow_read(input_file, format=None):
69 | """
70 | Reads optical flow from file
71 |
72 | Parameters
73 | ----------
74 | output_file: {str, pathlib.Path, file}
75 | Path of the file to read or file object.
76 | format: str, optional
77 | Specify in what format the flow is raed, accepted formats: "png" or "flo"
78 | If None, it is guess on the file extension
79 |
80 | Returns
81 | -------
82 | flow: numpy.ndarray
83 | 3D flow in the HWF (Height, Width, Flow) layout.
84 | flow[..., 0] is the x-displacement
85 | flow[..., 1] is the y-displacement
86 |
87 | Notes
88 | -----
89 |
90 | The flo format is dedicated to optical flow and was first used in Middlebury optical flow database.
91 | The original defition can be found here: http://vision.middlebury.edu/flow/code/flow-code/flowIO.cpp
92 |
93 | The png format uses 16-bit RGB png to store optical flows.
94 | It was developped along with the KITTI Vision Benchmark Suite.
95 | More information can be found here: http://www.cvlibs.net/datasets/kitti/eval_scene_flow.php?benchmark=flow
96 |
97 | The both handle flow with invalid ``invalid'' values, to deal with occlusion for example.
98 | We convert such invalid values to NaN.
99 |
100 | See Also
101 | --------
102 | flow_write
103 |
104 | """
105 |
106 | input_format = guess_extension(input_file, override=format)
107 |
108 | with FileManager(input_file, "rb") as f:
109 | if input_format == "png":
110 | output = flow_read_png(f)
111 | else:
112 | output = flow_read_flo(f)
113 |
114 | return output
115 |
116 |
117 | def flow_read_flo(f):
118 | if (f.read(4) != b'PIEH'):
119 | warn("{} does not have a .flo file signature".format(f.name))
120 |
121 | width, height = struct.unpack("II", f.read(8))
122 | result = np.fromfile(f, dtype="float32").reshape((height, width, 2))
123 |
124 | # Set invalid flows to NaN
125 | mask_u = np.greater(np.abs(result[..., 0]), 1e9, where=(~np.isnan(result[..., 0])))
126 | mask_v = np.greater(np.abs(result[..., 1]), 1e9, where=(~np.isnan(result[..., 1])))
127 |
128 | result[mask_u | mask_v] = np.NaN
129 |
130 | return result
131 |
132 |
133 | def flow_write_flo(f, flow):
134 | SENTINEL = 1666666800.0 # Only here to look like Middlebury original files
135 | height, width, _ = flow.shape
136 |
137 | image = flow.copy()
138 | image[np.isnan(image)] = SENTINEL
139 |
140 | f.write(b'PIEH')
141 | f.write(struct.pack("II", width, height))
142 | image.astype(np.float32).tofile(f)
143 |
144 |
145 | def flow_read_png(f):
146 | width, height, stream, *_ = png.Reader(f).read()
147 |
148 | file_content = np.concatenate(list(stream)).reshape((height, width, 3))
149 | flow, valid = file_content[..., 0:2], file_content[..., 2]
150 |
151 | flow = (flow.astype(np.float) - 2 ** 15) / 64.
152 |
153 | flow[~valid.astype(np.bool)] = np.NaN
154 |
155 | return flow
156 |
157 |
158 | def flow_write_png(f, flow):
159 | SENTINEL = 0. # Only here to look like original KITTI files
160 | height, width, _ = flow.shape
161 | flow_copy = flow.copy()
162 |
163 | valid = ~(np.isnan(flow[..., 0]) | np.isnan(flow[..., 1]))
164 | flow_copy[~valid] = SENTINEL
165 |
166 | flow_copy = (flow_copy * 64. + 2 ** 15).astype(np.uint16)
167 | image = np.dstack((flow_copy, valid))
168 |
169 | writer = png.Writer(width, height, bitdepth=16, greyscale=False)
170 | writer.write(f, image.reshape((height, 3 * width)))
171 |
172 |
173 | class FileManager:
174 | def __init__(self, abstract_file, mode):
175 | self.abstract_file = abstract_file
176 | self.opened_file = None
177 | self.mode = mode
178 |
179 | def __enter__(self):
180 | if isinstance(self.abstract_file, str):
181 | self.opened_file = open(self.abstract_file, self.mode)
182 | elif isinstance(self.abstract_file, Path):
183 | self.opened_file = self.abstract_file.open(self.mode)
184 | else:
185 | return self.abstract_file
186 |
187 | return self.opened_file
188 |
189 | def __exit__(self, exc_type, exc_value, traceback):
190 | if self.opened_file is not None:
191 | self.opened_file.close()
192 |
193 |
194 | def guess_extension(abstract_file, override=None):
195 | if override is not None:
196 | return override
197 |
198 | if isinstance(abstract_file, str):
199 | return Path(abstract_file).suffix[1:]
200 | elif isinstance(abstract_file, Path):
201 | return abstract_file.suffix[1:]
202 |
203 | return Path(abstract_file.name).suffix[1:]
204 |
205 |
206 |
208 |
210 |
211 |
212 |
213 |
214 | def flow_read (input_file, format=None)
215 |
216 |
217 | Reads optical flow from file
218 |
Parameters
219 |
220 | output_file : {str, pathlib.Path, file}
221 | Path of the file to read or file object.
222 | format : str, optional
223 | Specify in what format the flow is raed, accepted formats: "png" or "flo"
224 | If None, it is guess on the file extension
225 |
226 |
Returns
227 |
228 | flow : numpy.ndarray
229 | 3D flow in the HWF (Height, Width, Flow) layout.
230 | flow[…, 0] is the x-displacement
231 | flow[…, 1] is the y-displacement
232 |
233 |
Notes
234 |
The flo format is dedicated to optical flow and was first used in Middlebury optical flow database.
235 | The original defition can be found here: http://vision.middlebury.edu/flow/code/flow-code/flowIO.cpp
236 |
The png format uses 16-bit RGB png to store optical flows.
237 | It was developped along with the KITTI Vision Benchmark Suite.
238 | More information can be found here: http://www.cvlibs.net/datasets/kitti/eval_scene_flow.php?benchmark=flow
239 |
The both handle flow with invalid ``invalid'' values, to deal with occlusion for example.
240 | We convert such invalid values to NaN.
241 |
See Also
242 |
flow_write()
243 |
244 |
245 | Expand source code
246 |
247 | def flow_read(input_file, format=None):
248 | """
249 | Reads optical flow from file
250 |
251 | Parameters
252 | ----------
253 | output_file: {str, pathlib.Path, file}
254 | Path of the file to read or file object.
255 | format: str, optional
256 | Specify in what format the flow is raed, accepted formats: "png" or "flo"
257 | If None, it is guess on the file extension
258 |
259 | Returns
260 | -------
261 | flow: numpy.ndarray
262 | 3D flow in the HWF (Height, Width, Flow) layout.
263 | flow[..., 0] is the x-displacement
264 | flow[..., 1] is the y-displacement
265 |
266 | Notes
267 | -----
268 |
269 | The flo format is dedicated to optical flow and was first used in Middlebury optical flow database.
270 | The original defition can be found here: http://vision.middlebury.edu/flow/code/flow-code/flowIO.cpp
271 |
272 | The png format uses 16-bit RGB png to store optical flows.
273 | It was developped along with the KITTI Vision Benchmark Suite.
274 | More information can be found here: http://www.cvlibs.net/datasets/kitti/eval_scene_flow.php?benchmark=flow
275 |
276 | The both handle flow with invalid ``invalid'' values, to deal with occlusion for example.
277 | We convert such invalid values to NaN.
278 |
279 | See Also
280 | --------
281 | flow_write
282 |
283 | """
284 |
285 | input_format = guess_extension(input_file, override=format)
286 |
287 | with FileManager(input_file, "rb") as f:
288 | if input_format == "png":
289 | output = flow_read_png(f)
290 | else:
291 | output = flow_read_flo(f)
292 |
293 | return output
294 |
295 |
296 |
297 | def flow_read_flo (f)
298 |
299 |
300 |
301 |
302 |
303 | Expand source code
304 |
305 | def flow_read_flo(f):
306 | if (f.read(4) != b'PIEH'):
307 | warn("{} does not have a .flo file signature".format(f.name))
308 |
309 | width, height = struct.unpack("II", f.read(8))
310 | result = np.fromfile(f, dtype="float32").reshape((height, width, 2))
311 |
312 | # Set invalid flows to NaN
313 | mask_u = np.greater(np.abs(result[..., 0]), 1e9, where=(~np.isnan(result[..., 0])))
314 | mask_v = np.greater(np.abs(result[..., 1]), 1e9, where=(~np.isnan(result[..., 1])))
315 |
316 | result[mask_u | mask_v] = np.NaN
317 |
318 | return result
319 |
320 |
321 |
322 | def flow_read_png (f)
323 |
324 |
325 |
326 |
327 |
328 | Expand source code
329 |
330 | def flow_read_png(f):
331 | width, height, stream, *_ = png.Reader(f).read()
332 |
333 | file_content = np.concatenate(list(stream)).reshape((height, width, 3))
334 | flow, valid = file_content[..., 0:2], file_content[..., 2]
335 |
336 | flow = (flow.astype(np.float) - 2 ** 15) / 64.
337 |
338 | flow[~valid.astype(np.bool)] = np.NaN
339 |
340 | return flow
341 |
342 |
343 |
344 | def flow_write (output_file, flow, format=None)
345 |
346 |
347 | Writes optical flow to file.
348 |
Parameters
349 |
350 | output_file : {str, pathlib.Path, file}
351 | Path of the file to write or file object.
352 | flow : numpy.ndarray
353 | 3D flow in the HWF (Height, Width, Flow) layout.
354 | flow[…, 0] should be the x-displacement
355 | flow[…, 1] should be the y-displacement
356 | format : str, optional
357 | Specify in what format the flow is written, accepted formats: "png" or "flo"
358 | If None, it is guessed on the file extension
359 |
360 |
See Also
361 |
flow_read()
362 |
363 |
364 | Expand source code
365 |
366 | def flow_write(output_file, flow, format=None):
367 | """
368 | Writes optical flow to file.
369 |
370 | Parameters
371 | ----------
372 | output_file: {str, pathlib.Path, file}
373 | Path of the file to write or file object.
374 | flow: numpy.ndarray
375 | 3D flow in the HWF (Height, Width, Flow) layout.
376 | flow[..., 0] should be the x-displacement
377 | flow[..., 1] should be the y-displacement
378 | format: str, optional
379 | Specify in what format the flow is written, accepted formats: "png" or "flo"
380 | If None, it is guessed on the file extension
381 |
382 | See Also
383 | --------
384 | flow_read
385 |
386 | """
387 |
388 | supported_formats = ("png", "flo")
389 |
390 | output_format = guess_extension(output_file, override=format)
391 |
392 | with FileManager(output_file, "wb") as f:
393 | if output_format == "png":
394 | flow_write_png(f, flow)
395 | else:
396 | flow_write_flo(f, flow)
397 |
398 |
399 |
400 | def flow_write_flo (f, flow)
401 |
402 |
403 |
404 |
405 |
406 | Expand source code
407 |
408 | def flow_write_flo(f, flow):
409 | SENTINEL = 1666666800.0 # Only here to look like Middlebury original files
410 | height, width, _ = flow.shape
411 |
412 | image = flow.copy()
413 | image[np.isnan(image)] = SENTINEL
414 |
415 | f.write(b'PIEH')
416 | f.write(struct.pack("II", width, height))
417 | image.astype(np.float32).tofile(f)
418 |
419 |
420 |
421 | def flow_write_png (f, flow)
422 |
423 |
424 |
425 |
426 |
427 | Expand source code
428 |
429 | def flow_write_png(f, flow):
430 | SENTINEL = 0. # Only here to look like original KITTI files
431 | height, width, _ = flow.shape
432 | flow_copy = flow.copy()
433 |
434 | valid = ~(np.isnan(flow[..., 0]) | np.isnan(flow[..., 1]))
435 | flow_copy[~valid] = SENTINEL
436 |
437 | flow_copy = (flow_copy * 64. + 2 ** 15).astype(np.uint16)
438 | image = np.dstack((flow_copy, valid))
439 |
440 | writer = png.Writer(width, height, bitdepth=16, greyscale=False)
441 | writer.write(f, image.reshape((height, 3 * width)))
442 |
443 |
444 |
445 | def guess_extension (abstract_file, override=None)
446 |
447 |
448 |
449 |
450 |
451 | Expand source code
452 |
453 | def guess_extension(abstract_file, override=None):
454 | if override is not None:
455 | return override
456 |
457 | if isinstance(abstract_file, str):
458 | return Path(abstract_file).suffix[1:]
459 | elif isinstance(abstract_file, Path):
460 | return abstract_file.suffix[1:]
461 |
462 | return Path(abstract_file.name).suffix[1:]
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 | class FileManager
472 | ( abstract_file, mode)
473 |
474 |
475 |
476 |
477 |
478 | Expand source code
479 |
480 | class FileManager:
481 | def __init__(self, abstract_file, mode):
482 | self.abstract_file = abstract_file
483 | self.opened_file = None
484 | self.mode = mode
485 |
486 | def __enter__(self):
487 | if isinstance(self.abstract_file, str):
488 | self.opened_file = open(self.abstract_file, self.mode)
489 | elif isinstance(self.abstract_file, Path):
490 | self.opened_file = self.abstract_file.open(self.mode)
491 | else:
492 | return self.abstract_file
493 |
494 | return self.opened_file
495 |
496 | def __exit__(self, exc_type, exc_value, traceback):
497 | if self.opened_file is not None:
498 | self.opened_file.close()
499 |
500 |
501 |
502 |
503 |
504 |
535 |
536 |
539 |
540 |
541 |
542 |
--------------------------------------------------------------------------------
/docs/flowpy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | flowpy.flowpy API documentation
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Module flowpy.flowpy
21 |
22 |
23 |
24 |
25 | Expand source code
26 |
27 | import matplotlib.pyplot as plt
28 | import numpy as np
29 |
30 | from collections import namedtuple
31 | from itertools import accumulate
32 | from matplotlib.ticker import AutoMinorLocator
33 |
34 | DEFAULT_TRANSITIONS = (15, 6, 4, 11, 13, 6)
35 |
36 |
37 | def flow_to_rgb(flow, flow_max_radius=None, background="bright", custom_colorwheel=None):
38 | """
39 | Creates a RGB representation of an optical flow.
40 |
41 | Parameters
42 | ----------
43 | flow: numpy.ndarray
44 | 3D flow in the HWF (Height, Width, Flow) layout.
45 | flow[..., 0] should be the x-displacement
46 | flow[..., 1] should be the y-displacement
47 |
48 | flow_max_radius: float, optionnal
49 | Set the radius that gives the maximum color intensity, useful for comparing different flows.
50 | Default: The normalization is based on the input flow maximum radius.
51 |
52 | background: str, optionnal
53 | States if zero-valued flow should look 'bright' or 'dark'
54 | Default: "bright"
55 |
56 | custom_colorwheel: numpy.ndarray
57 | Use a custom colorwheel for specific hue transition lengths.
58 | By default, the default transition lengths are used.
59 |
60 | Returns
61 | -------
62 | rgb_image: numpy.ndarray
63 | A 2D RGB image that represents the flow
64 |
65 | See Also
66 | --------
67 | make_colorwheel
68 |
69 | """
70 |
71 | valid_backgrounds = ("bright", "dark")
72 | if background not in valid_backgrounds:
73 | raise ValueError("background should be one the following: {}, not {}".format(
74 | valid_backgrounds, background))
75 |
76 | wheel = make_colorwheel() if custom_colorwheel is None else custom_colorwheel
77 |
78 | flow_height, flow_width, _ = flow.shape
79 |
80 | complex_flow = flow[..., 0] + 1j * flow[..., 1]
81 | complex_flow, nan_mask = replace_nans(complex_flow)
82 |
83 | radius, angle = np.abs(complex_flow), np.angle(complex_flow)
84 |
85 | if flow_max_radius is None:
86 | flow_max_radius = np.max(radius)
87 |
88 | if flow_max_radius > 0:
89 | radius /= flow_max_radius
90 |
91 | ncols = len(wheel)
92 |
93 | # Map the angles from (-pi, pi] to [0, ncols - 1)
94 | angle = (-angle + np.pi) * ((ncols - 1) / (2 * np.pi))
95 |
96 | # Interpolate the hues
97 | (angle_fractional, angle_floor), angle_ceil = np.modf(angle), np.ceil(angle)
98 | angle_fractional = angle_fractional.reshape((angle_fractional.shape) + (1,))
99 | float_hue = (wheel[angle_floor.astype(np.int)] * (1 - angle_fractional) +
100 | wheel[angle_ceil.astype(np.int)] * angle_fractional)
101 |
102 | ColorizationArgs = namedtuple("ColorizationArgs", [
103 | 'move_hue_valid_radius',
104 | 'move_hue_oversized_radius',
105 | 'invalid_color'])
106 |
107 | def move_hue_on_V_axis(hues, factors):
108 | return hues * np.expand_dims(factors, -1)
109 |
110 | def move_hue_on_S_axis(hues, factors):
111 | return 255. - np.expand_dims(factors, -1) * (255. - hues)
112 |
113 | if background == "dark":
114 | parameters = ColorizationArgs(move_hue_on_V_axis, move_hue_on_S_axis,
115 | np.array([255, 255, 255], dtype=np.float))
116 | else:
117 | parameters = ColorizationArgs(move_hue_on_S_axis, move_hue_on_V_axis,
118 | np.array([0, 0, 0], dtype=np.float))
119 |
120 | colors = parameters.move_hue_valid_radius(float_hue, radius)
121 |
122 | oversized_radius_mask = radius > 1
123 | colors[oversized_radius_mask] = parameters.move_hue_oversized_radius(
124 | float_hue[oversized_radius_mask],
125 | 1 / radius[oversized_radius_mask]
126 | )
127 | colors[nan_mask] = parameters.invalid_color
128 |
129 | return colors.astype(np.uint8)
130 |
131 |
132 | def make_colorwheel(transitions=DEFAULT_TRANSITIONS):
133 | """
134 | Creates a color wheel.
135 |
136 | A color wheel defines the transitions between the six primary hues:
137 | Red(255, 0, 0), Yellow(255, 255, 0), Green(0, 255, 0), Cyan(0, 255, 255), Blue(0, 0, 255) and Magenta(255, 0, 255).
138 |
139 | Parameters
140 | ----------
141 | transitions: sequence_like
142 | Contains the length of the six transitions.
143 | Defaults to (15, 6, 4, 11, 13, 6), based on humain perception.
144 |
145 | Returns
146 | -------
147 | colorwheel: numpy.ndarray
148 | The RGB values of the transitions in the color space.
149 |
150 | Notes
151 | -----
152 | For more information, take a look at
153 | https://web.archive.org/web/20051107102013/http://members.shaw.ca/quadibloc/other/colint.htm
154 |
155 | """
156 |
157 | colorwheel_length = sum(transitions)
158 |
159 | # The red hue is repeated to make the color wheel cyclic
160 | base_hues = map(np.array,
161 | ([255, 0, 0], [255, 255, 0], [0, 255, 0],
162 | [0, 255, 255], [0, 0, 255], [255, 0, 255],
163 | [255, 0, 0]))
164 |
165 | colorwheel = np.zeros((colorwheel_length, 3), dtype="uint8")
166 | hue_from = next(base_hues)
167 | start_index = 0
168 | for hue_to, end_index in zip(base_hues, accumulate(transitions)):
169 | transition_length = end_index - start_index
170 |
171 | colorwheel[start_index:end_index] = np.linspace(
172 | hue_from, hue_to, transition_length, endpoint=False)
173 | hue_from = hue_to
174 | start_index = end_index
175 |
176 | return colorwheel
177 |
178 |
179 | def calibration_pattern(pixel_size=151, flow_max_radius=1, **flow_to_rgb_args):
180 | """
181 | Generates a calibration pattern.
182 |
183 | Useful to add a legend to your optical flow plots.
184 |
185 | Parameters
186 | ----------
187 | pixel_size: int
188 | Radius of the square test pattern.
189 | flow_max_radius: float
190 | The maximum radius value represented by the calibration pattern.
191 | flow_to_rgb_args: kwargs
192 | Arguments passed to the flow_to_rgb function
193 |
194 | Returns
195 | -------
196 | calibration_img: numpy.ndarray
197 | The RGB image representation of the calibration pattern.
198 | calibration_flow: numpy.ndarray
199 | The flow represented in the calibration_pattern. In HWF layout
200 |
201 | """
202 | half_width = pixel_size // 2
203 |
204 | y_grid, x_grid = np.mgrid[:pixel_size, :pixel_size]
205 |
206 | u = flow_max_radius * (x_grid / half_width - 1)
207 | v = flow_max_radius * (y_grid / half_width - 1)
208 |
209 | flow = np.zeros((pixel_size, pixel_size, 2))
210 | flow[..., 0] = u
211 | flow[..., 1] = v
212 |
213 | flow_to_rgb_args["flow_max_radius"] = flow_max_radius
214 | img = flow_to_rgb(flow, **flow_to_rgb_args)
215 |
216 | return img, flow
217 |
218 |
219 | def attach_arrows(ax, flow, xy_steps=(20, 20),
220 | units="xy", color="w", angles="xy", **quiver_kwargs):
221 | """
222 | Attach the flow arrows to a matplotlib axes using quiver.
223 |
224 | Parameters:
225 | -----------
226 | ax: matplotlib.axes
227 | The axes the arrows should be plotted on.
228 | flow: numpy.ndarray
229 | 3D flow in the HWF (Height, Width, Flow) layout.
230 | flow[..., 0] should be the x-displacement
231 | flow[..., 1] should be the y-displacement
232 | xy_steps: sequence_like
233 | The arrows are plotted every xy_steps[0] in the x-dimension and xy_steps[1] in the y-dimension
234 |
235 | Quiver Parameters:
236 | ------------------
237 | The following parameters are here to override matplotlib.quiver's defaults.
238 | units: str
239 | See matplotlib.quiver documentation.
240 | color: str
241 | See matplotlib.quiver documentation.
242 | angles: str
243 | See matplotlib.quiver documentation.
244 | quiver_kwargs: kwargs
245 | Other parameters passed to matplotlib.quiver
246 | See matplotlib.quiver documentation.
247 |
248 | Returns
249 | -------
250 | quiver_artist: matplotlib.artist
251 | See matplotlib.quiver documentation
252 | Useful for removing the arrows from the figure
253 |
254 | """
255 | height, width, _ = flow.shape
256 |
257 | y_grid, x_grid = np.mgrid[:height, :width]
258 |
259 | step_x, step_y = xy_steps
260 | half_step_x, half_step_y = step_x // 2, step_y // 2
261 |
262 | return ax.quiver(
263 | x_grid[half_step_x::step_x, half_step_y::step_y],
264 | y_grid[half_step_x::step_x, half_step_y::step_y],
265 | flow[half_step_x::step_x, half_step_y::step_y, 0],
266 | flow[half_step_x::step_x, half_step_y::step_y, 1],
267 | angles=angles,
268 | units=units, color=color, **quiver_kwargs,
269 | )
270 |
271 |
272 | def attach_coord(ax, flow, extent=None):
273 | """
274 | Attach the flow value to the coordinate tooltip.
275 |
276 | It allows you to see on the same figure, the RGB value of the pixel and the underlying value of the flow.
277 | Shows cartesian and polar coordinates.
278 |
279 | Parameters:
280 | -----------
281 | ax: matplotlib.axes
282 | The axes the arrows should be plotted on.
283 | flow: numpy.ndarray
284 | 3D flow in the HWF (Height, Width, Flow) layout.
285 | flow[..., 0] should be the x-displacement
286 | flow[..., 1] should be the y-displacement
287 | extent: sequence_like, optional
288 | Use this parameters in combination with matplotlib.imshow to resize the RGB plot.
289 | See matplotlib.imshow extent parameter.
290 | See attach_calibration_pattern
291 |
292 | """
293 | height, width, _ = flow.shape
294 | base_format = ax.format_coord
295 | if extent is not None:
296 | left, right, bottom, top = extent
297 | x_ratio = width / (right - left)
298 | y_ratio = height / (top - bottom)
299 |
300 | def new_format_coord(x, y):
301 | if extent is None:
302 | int_x = int(x + 0.5)
303 | int_y = int(y + 0.5)
304 | else:
305 | int_x = int((x - left) * x_ratio)
306 | int_y = int((y - bottom) * y_ratio)
307 |
308 | if 0 <= int_x < width and 0 <= int_y < height:
309 | format_string = "Coord: x={}, y={} / Flow: ".format(int_x, int_y)
310 |
311 | u, v = flow[int_y, int_x, :]
312 | if np.isnan(u) or np.isnan(v):
313 | format_string += "invalid"
314 | else:
315 | complex_flow = u - 1j * v
316 | r, h = np.abs(complex_flow), np.angle(complex_flow, deg=True)
317 | format_string += ("u={:.2f}, v={:.2f} (cartesian) ρ={:.2f}, θ={:.2f}° (polar)"
318 | .format(u, v, r, h))
319 | return format_string
320 | else:
321 | return base_format(x, y)
322 |
323 | ax.format_coord = new_format_coord
324 |
325 |
326 | def attach_calibration_pattern(ax, **calibration_pattern_kwargs):
327 | """
328 | Attach a calibration pattern to axes.
329 |
330 | This function uses calibration_pattern to generate a figure and shows it as nicely as possible.
331 |
332 | Parameters:
333 | -----------
334 | calibration_pattern_kwargs: kwargs, optional
335 | Parameters to be given to the calibration_pattern function.
336 |
337 | See Also:
338 | ---------
339 | calibration_pattern
340 |
341 | Returns
342 | -------
343 | image_axes: matplotlib.AxesImage
344 | See matplotlib.imshow documentation
345 | Useful for changing the image dynamically
346 | circle_artist: matplotlib.artist
347 | See matplotlib.circle documentation
348 | Useful for removing the circle from the figure
349 |
350 | """
351 | pattern, flow = calibration_pattern(**calibration_pattern_kwargs)
352 | flow_max_radius = calibration_pattern_kwargs.get("flow_max_radius", 1)
353 |
354 | extent = (-flow_max_radius, flow_max_radius) * 2
355 |
356 | image = ax.imshow(pattern, extent=extent)
357 | ax.spines["top"].set_visible(False)
358 | ax.spines["right"].set_visible(False)
359 |
360 | for spine in ("bottom", "left"):
361 | ax.spines[spine].set_position("zero")
362 | ax.spines[spine].set_linewidth(1)
363 |
364 | ax.xaxis.set_minor_locator(AutoMinorLocator())
365 | ax.yaxis.set_minor_locator(AutoMinorLocator())
366 |
367 | attach_coord(ax, flow, extent=extent)
368 |
369 | circle = plt.Circle((0, 0), flow_max_radius, fill=False, lw=1)
370 | ax.add_artist(circle)
371 |
372 | return image, circle
373 |
374 |
375 | def replace_nans(array, value=0):
376 | nan_mask = np.isnan(array)
377 | array[nan_mask] = value
378 |
379 | return array, nan_mask
380 |
381 |
382 | def get_flow_max_radius(flow):
383 | return np.sqrt(np.nanmax(np.sum(flow ** 2, axis=2)))
384 |
385 |
386 |
388 |
390 |
391 |
392 |
393 |
394 | def attach_arrows (ax, flow, xy_steps=(20, 20), units='xy', color='w', angles='xy', **quiver_kwargs)
395 |
396 |
397 | Attach the flow arrows to a matplotlib axes using quiver.
398 |
Parameters:
399 |
ax: matplotlib.axes
400 | The axes the arrows should be plotted on.
401 | flow: numpy.ndarray
402 | 3D flow in the HWF (Height, Width, Flow) layout.
403 | flow[…, 0] should be the x-displacement
404 | flow[…, 1] should be the y-displacement
405 | xy_steps: sequence_like
406 | The arrows are plotted every xy_steps[0] in the x-dimension and xy_steps[1] in the y-dimension
407 |
Quiver Parameters:
408 |
The following parameters are here to override matplotlib.quiver's defaults.
409 | units: str
410 | See matplotlib.quiver documentation.
411 | color: str
412 | See matplotlib.quiver documentation.
413 | angles: str
414 | See matplotlib.quiver documentation.
415 | quiver_kwargs: kwargs
416 | Other parameters passed to matplotlib.quiver
417 | See matplotlib.quiver documentation.
418 |
Returns
419 |
420 | quiver_artist : matplotlib.artist
421 | See matplotlib.quiver documentation
422 | Useful for removing the arrows from the figure
423 |
424 |
425 |
426 | Expand source code
427 |
428 | def attach_arrows(ax, flow, xy_steps=(20, 20),
429 | units="xy", color="w", angles="xy", **quiver_kwargs):
430 | """
431 | Attach the flow arrows to a matplotlib axes using quiver.
432 |
433 | Parameters:
434 | -----------
435 | ax: matplotlib.axes
436 | The axes the arrows should be plotted on.
437 | flow: numpy.ndarray
438 | 3D flow in the HWF (Height, Width, Flow) layout.
439 | flow[..., 0] should be the x-displacement
440 | flow[..., 1] should be the y-displacement
441 | xy_steps: sequence_like
442 | The arrows are plotted every xy_steps[0] in the x-dimension and xy_steps[1] in the y-dimension
443 |
444 | Quiver Parameters:
445 | ------------------
446 | The following parameters are here to override matplotlib.quiver's defaults.
447 | units: str
448 | See matplotlib.quiver documentation.
449 | color: str
450 | See matplotlib.quiver documentation.
451 | angles: str
452 | See matplotlib.quiver documentation.
453 | quiver_kwargs: kwargs
454 | Other parameters passed to matplotlib.quiver
455 | See matplotlib.quiver documentation.
456 |
457 | Returns
458 | -------
459 | quiver_artist: matplotlib.artist
460 | See matplotlib.quiver documentation
461 | Useful for removing the arrows from the figure
462 |
463 | """
464 | height, width, _ = flow.shape
465 |
466 | y_grid, x_grid = np.mgrid[:height, :width]
467 |
468 | step_x, step_y = xy_steps
469 | half_step_x, half_step_y = step_x // 2, step_y // 2
470 |
471 | return ax.quiver(
472 | x_grid[half_step_x::step_x, half_step_y::step_y],
473 | y_grid[half_step_x::step_x, half_step_y::step_y],
474 | flow[half_step_x::step_x, half_step_y::step_y, 0],
475 | flow[half_step_x::step_x, half_step_y::step_y, 1],
476 | angles=angles,
477 | units=units, color=color, **quiver_kwargs,
478 | )
479 |
480 |
481 |
482 | def attach_calibration_pattern (ax, **calibration_pattern_kwargs)
483 |
484 |
485 | Attach a calibration pattern to axes.
486 |
This function uses calibration_pattern to generate a figure and shows it as nicely as possible.
487 |
Parameters:
488 |
calibration_pattern_kwargs: kwargs, optional
489 | Parameters to be given to the calibration_pattern function.
490 |
See Also:
491 |
calibration_pattern
492 |
Returns
493 |
494 | image_axes : matplotlib.AxesImage
495 | See matplotlib.imshow documentation
496 | Useful for changing the image dynamically
497 | circle_artist : matplotlib.artist
498 | See matplotlib.circle documentation
499 | Useful for removing the circle from the figure
500 |
501 |
502 |
503 | Expand source code
504 |
505 | def attach_calibration_pattern(ax, **calibration_pattern_kwargs):
506 | """
507 | Attach a calibration pattern to axes.
508 |
509 | This function uses calibration_pattern to generate a figure and shows it as nicely as possible.
510 |
511 | Parameters:
512 | -----------
513 | calibration_pattern_kwargs: kwargs, optional
514 | Parameters to be given to the calibration_pattern function.
515 |
516 | See Also:
517 | ---------
518 | calibration_pattern
519 |
520 | Returns
521 | -------
522 | image_axes: matplotlib.AxesImage
523 | See matplotlib.imshow documentation
524 | Useful for changing the image dynamically
525 | circle_artist: matplotlib.artist
526 | See matplotlib.circle documentation
527 | Useful for removing the circle from the figure
528 |
529 | """
530 | pattern, flow = calibration_pattern(**calibration_pattern_kwargs)
531 | flow_max_radius = calibration_pattern_kwargs.get("flow_max_radius", 1)
532 |
533 | extent = (-flow_max_radius, flow_max_radius) * 2
534 |
535 | image = ax.imshow(pattern, extent=extent)
536 | ax.spines["top"].set_visible(False)
537 | ax.spines["right"].set_visible(False)
538 |
539 | for spine in ("bottom", "left"):
540 | ax.spines[spine].set_position("zero")
541 | ax.spines[spine].set_linewidth(1)
542 |
543 | ax.xaxis.set_minor_locator(AutoMinorLocator())
544 | ax.yaxis.set_minor_locator(AutoMinorLocator())
545 |
546 | attach_coord(ax, flow, extent=extent)
547 |
548 | circle = plt.Circle((0, 0), flow_max_radius, fill=False, lw=1)
549 | ax.add_artist(circle)
550 |
551 | return image, circle
552 |
553 |
554 |
555 | def attach_coord (ax, flow, extent=None)
556 |
557 |
558 | Attach the flow value to the coordinate tooltip.
559 |
It allows you to see on the same figure, the RGB value of the pixel and the underlying value of the flow.
560 | Shows cartesian and polar coordinates.
561 |
Parameters:
562 |
ax: matplotlib.axes
563 | The axes the arrows should be plotted on.
564 | flow: numpy.ndarray
565 | 3D flow in the HWF (Height, Width, Flow) layout.
566 | flow[…, 0] should be the x-displacement
567 | flow[…, 1] should be the y-displacement
568 | extent: sequence_like, optional
569 | Use this parameters in combination with matplotlib.imshow to resize the RGB plot.
570 | See matplotlib.imshow extent parameter.
571 | See attach_calibration_pattern
572 |
573 |
574 | Expand source code
575 |
576 | def attach_coord(ax, flow, extent=None):
577 | """
578 | Attach the flow value to the coordinate tooltip.
579 |
580 | It allows you to see on the same figure, the RGB value of the pixel and the underlying value of the flow.
581 | Shows cartesian and polar coordinates.
582 |
583 | Parameters:
584 | -----------
585 | ax: matplotlib.axes
586 | The axes the arrows should be plotted on.
587 | flow: numpy.ndarray
588 | 3D flow in the HWF (Height, Width, Flow) layout.
589 | flow[..., 0] should be the x-displacement
590 | flow[..., 1] should be the y-displacement
591 | extent: sequence_like, optional
592 | Use this parameters in combination with matplotlib.imshow to resize the RGB plot.
593 | See matplotlib.imshow extent parameter.
594 | See attach_calibration_pattern
595 |
596 | """
597 | height, width, _ = flow.shape
598 | base_format = ax.format_coord
599 | if extent is not None:
600 | left, right, bottom, top = extent
601 | x_ratio = width / (right - left)
602 | y_ratio = height / (top - bottom)
603 |
604 | def new_format_coord(x, y):
605 | if extent is None:
606 | int_x = int(x + 0.5)
607 | int_y = int(y + 0.5)
608 | else:
609 | int_x = int((x - left) * x_ratio)
610 | int_y = int((y - bottom) * y_ratio)
611 |
612 | if 0 <= int_x < width and 0 <= int_y < height:
613 | format_string = "Coord: x={}, y={} / Flow: ".format(int_x, int_y)
614 |
615 | u, v = flow[int_y, int_x, :]
616 | if np.isnan(u) or np.isnan(v):
617 | format_string += "invalid"
618 | else:
619 | complex_flow = u - 1j * v
620 | r, h = np.abs(complex_flow), np.angle(complex_flow, deg=True)
621 | format_string += ("u={:.2f}, v={:.2f} (cartesian) ρ={:.2f}, θ={:.2f}° (polar)"
622 | .format(u, v, r, h))
623 | return format_string
624 | else:
625 | return base_format(x, y)
626 |
627 | ax.format_coord = new_format_coord
628 |
629 |
630 |
631 | def calibration_pattern (pixel_size=151, flow_max_radius=1, **flow_to_rgb_args)
632 |
633 |
634 | Generates a calibration pattern.
635 |
Useful to add a legend to your optical flow plots.
636 |
Parameters
637 |
638 | pixel_size : int
639 | Radius of the square test pattern.
640 | flow_max_radius : float
641 | The maximum radius value represented by the calibration pattern.
642 | flow_to_rgb_args : kwargs
643 | Arguments passed to the flow_to_rgb function
644 |
645 |
Returns
646 |
647 | calibration_img : numpy.ndarray
648 | The RGB image representation of the calibration pattern.
649 | calibration_flow : numpy.ndarray
650 | The flow represented in the calibration_pattern. In HWF layout
651 |
652 |
653 |
654 | Expand source code
655 |
656 | def calibration_pattern(pixel_size=151, flow_max_radius=1, **flow_to_rgb_args):
657 | """
658 | Generates a calibration pattern.
659 |
660 | Useful to add a legend to your optical flow plots.
661 |
662 | Parameters
663 | ----------
664 | pixel_size: int
665 | Radius of the square test pattern.
666 | flow_max_radius: float
667 | The maximum radius value represented by the calibration pattern.
668 | flow_to_rgb_args: kwargs
669 | Arguments passed to the flow_to_rgb function
670 |
671 | Returns
672 | -------
673 | calibration_img: numpy.ndarray
674 | The RGB image representation of the calibration pattern.
675 | calibration_flow: numpy.ndarray
676 | The flow represented in the calibration_pattern. In HWF layout
677 |
678 | """
679 | half_width = pixel_size // 2
680 |
681 | y_grid, x_grid = np.mgrid[:pixel_size, :pixel_size]
682 |
683 | u = flow_max_radius * (x_grid / half_width - 1)
684 | v = flow_max_radius * (y_grid / half_width - 1)
685 |
686 | flow = np.zeros((pixel_size, pixel_size, 2))
687 | flow[..., 0] = u
688 | flow[..., 1] = v
689 |
690 | flow_to_rgb_args["flow_max_radius"] = flow_max_radius
691 | img = flow_to_rgb(flow, **flow_to_rgb_args)
692 |
693 | return img, flow
694 |
695 |
696 |
697 | def flow_to_rgb (flow, flow_max_radius=None, background='bright', custom_colorwheel=None)
698 |
699 |
700 | Creates a RGB representation of an optical flow.
701 |
Parameters
702 |
703 | flow : numpy.ndarray
704 | 3D flow in the HWF (Height, Width, Flow) layout.
705 | flow[…, 0] should be the x-displacement
706 | flow[…, 1] should be the y-displacement
707 | flow_max_radius : float, optionnal
708 | Set the radius that gives the maximum color intensity, useful for comparing different flows.
709 | Default: The normalization is based on the input flow maximum radius.
710 | background : str, optionnal
711 | States if zero-valued flow should look 'bright' or 'dark'
712 | Default: "bright"
713 | custom_colorwheel : numpy.ndarray
714 | Use a custom colorwheel for specific hue transition lengths.
715 | By default, the default transition lengths are used.
716 |
717 |
Returns
718 |
719 | rgb_image : numpy.ndarray
720 | A 2D RGB image that represents the flow
721 |
722 |
See Also
723 |
make_colorwheel()
724 |
725 |
726 | Expand source code
727 |
728 | def flow_to_rgb(flow, flow_max_radius=None, background="bright", custom_colorwheel=None):
729 | """
730 | Creates a RGB representation of an optical flow.
731 |
732 | Parameters
733 | ----------
734 | flow: numpy.ndarray
735 | 3D flow in the HWF (Height, Width, Flow) layout.
736 | flow[..., 0] should be the x-displacement
737 | flow[..., 1] should be the y-displacement
738 |
739 | flow_max_radius: float, optionnal
740 | Set the radius that gives the maximum color intensity, useful for comparing different flows.
741 | Default: The normalization is based on the input flow maximum radius.
742 |
743 | background: str, optionnal
744 | States if zero-valued flow should look 'bright' or 'dark'
745 | Default: "bright"
746 |
747 | custom_colorwheel: numpy.ndarray
748 | Use a custom colorwheel for specific hue transition lengths.
749 | By default, the default transition lengths are used.
750 |
751 | Returns
752 | -------
753 | rgb_image: numpy.ndarray
754 | A 2D RGB image that represents the flow
755 |
756 | See Also
757 | --------
758 | make_colorwheel
759 |
760 | """
761 |
762 | valid_backgrounds = ("bright", "dark")
763 | if background not in valid_backgrounds:
764 | raise ValueError("background should be one the following: {}, not {}".format(
765 | valid_backgrounds, background))
766 |
767 | wheel = make_colorwheel() if custom_colorwheel is None else custom_colorwheel
768 |
769 | flow_height, flow_width, _ = flow.shape
770 |
771 | complex_flow = flow[..., 0] + 1j * flow[..., 1]
772 | complex_flow, nan_mask = replace_nans(complex_flow)
773 |
774 | radius, angle = np.abs(complex_flow), np.angle(complex_flow)
775 |
776 | if flow_max_radius is None:
777 | flow_max_radius = np.max(radius)
778 |
779 | if flow_max_radius > 0:
780 | radius /= flow_max_radius
781 |
782 | ncols = len(wheel)
783 |
784 | # Map the angles from (-pi, pi] to [0, ncols - 1)
785 | angle = (-angle + np.pi) * ((ncols - 1) / (2 * np.pi))
786 |
787 | # Interpolate the hues
788 | (angle_fractional, angle_floor), angle_ceil = np.modf(angle), np.ceil(angle)
789 | angle_fractional = angle_fractional.reshape((angle_fractional.shape) + (1,))
790 | float_hue = (wheel[angle_floor.astype(np.int)] * (1 - angle_fractional) +
791 | wheel[angle_ceil.astype(np.int)] * angle_fractional)
792 |
793 | ColorizationArgs = namedtuple("ColorizationArgs", [
794 | 'move_hue_valid_radius',
795 | 'move_hue_oversized_radius',
796 | 'invalid_color'])
797 |
798 | def move_hue_on_V_axis(hues, factors):
799 | return hues * np.expand_dims(factors, -1)
800 |
801 | def move_hue_on_S_axis(hues, factors):
802 | return 255. - np.expand_dims(factors, -1) * (255. - hues)
803 |
804 | if background == "dark":
805 | parameters = ColorizationArgs(move_hue_on_V_axis, move_hue_on_S_axis,
806 | np.array([255, 255, 255], dtype=np.float))
807 | else:
808 | parameters = ColorizationArgs(move_hue_on_S_axis, move_hue_on_V_axis,
809 | np.array([0, 0, 0], dtype=np.float))
810 |
811 | colors = parameters.move_hue_valid_radius(float_hue, radius)
812 |
813 | oversized_radius_mask = radius > 1
814 | colors[oversized_radius_mask] = parameters.move_hue_oversized_radius(
815 | float_hue[oversized_radius_mask],
816 | 1 / radius[oversized_radius_mask]
817 | )
818 | colors[nan_mask] = parameters.invalid_color
819 |
820 | return colors.astype(np.uint8)
821 |
822 |
823 |
824 | def get_flow_max_radius (flow)
825 |
826 |
827 |
828 |
829 |
830 | Expand source code
831 |
832 | def get_flow_max_radius(flow):
833 | return np.sqrt(np.nanmax(np.sum(flow ** 2, axis=2)))
834 |
835 |
836 |
837 | def make_colorwheel (transitions=(15, 6, 4, 11, 13, 6))
838 |
839 |
840 | Creates a color wheel.
841 |
A color wheel defines the transitions between the six primary hues:
842 | Red(255, 0, 0), Yellow(255, 255, 0), Green(0, 255, 0), Cyan(0, 255, 255), Blue(0, 0, 255) and Magenta(255, 0, 255).
843 |
Parameters
844 |
845 | transitions : sequence_like
846 | Contains the length of the six transitions.
847 | Defaults to (15, 6, 4, 11, 13, 6), based on humain perception.
848 |
849 |
Returns
850 |
851 | colorwheel : numpy.ndarray
852 | The RGB values of the transitions in the color space.
853 |
854 |
Notes
855 |
For more information, take a look at
856 | https://web.archive.org/web/20051107102013/http://members.shaw.ca/quadibloc/other/colint.htm
857 |
858 |
859 | Expand source code
860 |
861 | def make_colorwheel(transitions=DEFAULT_TRANSITIONS):
862 | """
863 | Creates a color wheel.
864 |
865 | A color wheel defines the transitions between the six primary hues:
866 | Red(255, 0, 0), Yellow(255, 255, 0), Green(0, 255, 0), Cyan(0, 255, 255), Blue(0, 0, 255) and Magenta(255, 0, 255).
867 |
868 | Parameters
869 | ----------
870 | transitions: sequence_like
871 | Contains the length of the six transitions.
872 | Defaults to (15, 6, 4, 11, 13, 6), based on humain perception.
873 |
874 | Returns
875 | -------
876 | colorwheel: numpy.ndarray
877 | The RGB values of the transitions in the color space.
878 |
879 | Notes
880 | -----
881 | For more information, take a look at
882 | https://web.archive.org/web/20051107102013/http://members.shaw.ca/quadibloc/other/colint.htm
883 |
884 | """
885 |
886 | colorwheel_length = sum(transitions)
887 |
888 | # The red hue is repeated to make the color wheel cyclic
889 | base_hues = map(np.array,
890 | ([255, 0, 0], [255, 255, 0], [0, 255, 0],
891 | [0, 255, 255], [0, 0, 255], [255, 0, 255],
892 | [255, 0, 0]))
893 |
894 | colorwheel = np.zeros((colorwheel_length, 3), dtype="uint8")
895 | hue_from = next(base_hues)
896 | start_index = 0
897 | for hue_to, end_index in zip(base_hues, accumulate(transitions)):
898 | transition_length = end_index - start_index
899 |
900 | colorwheel[start_index:end_index] = np.linspace(
901 | hue_from, hue_to, transition_length, endpoint=False)
902 | hue_from = hue_to
903 | start_index = end_index
904 |
905 | return colorwheel
906 |
907 |
908 |
909 | def replace_nans (array, value=0)
910 |
911 |
912 |
913 |
914 |
915 | Expand source code
916 |
917 | def replace_nans(array, value=0):
918 | nan_mask = np.isnan(array)
919 | array[nan_mask] = value
920 |
921 | return array, nan_mask
922 |
923 |
924 |
925 |
926 |
928 |
929 |
954 |
955 |
958 |
959 |
960 |
961 |
--------------------------------------------------------------------------------