Skip to content

Commit 0916354

Browse files
committed
🎉 initial commit
0 parents  commit 0916354

10 files changed

+1306
-0
lines changed

.gitignore

+253
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
2+
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode
3+
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,visualstudiocode
4+
5+
### PyCharm+all ###
6+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
7+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8+
9+
# User-specific stuff
10+
.idea/**/workspace.xml
11+
.idea/**/tasks.xml
12+
.idea/**/usage.statistics.xml
13+
.idea/**/dictionaries
14+
.idea/**/shelf
15+
16+
# AWS User-specific
17+
.idea/**/aws.xml
18+
19+
# Generated files
20+
.idea/**/contentModel.xml
21+
22+
# Sensitive or high-churn files
23+
.idea/**/dataSources/
24+
.idea/**/dataSources.ids
25+
.idea/**/dataSources.local.xml
26+
.idea/**/sqlDataSources.xml
27+
.idea/**/dynamic.xml
28+
.idea/**/uiDesigner.xml
29+
.idea/**/dbnavigator.xml
30+
31+
# Gradle
32+
.idea/**/gradle.xml
33+
.idea/**/libraries
34+
35+
# Gradle and Maven with auto-import
36+
# When using Gradle or Maven with auto-import, you should exclude module files,
37+
# since they will be recreated, and may cause churn. Uncomment if using
38+
# auto-import.
39+
# .idea/artifacts
40+
# .idea/compiler.xml
41+
# .idea/jarRepositories.xml
42+
# .idea/modules.xml
43+
# .idea/*.iml
44+
# .idea/modules
45+
# *.iml
46+
# *.ipr
47+
48+
# CMake
49+
cmake-build-*/
50+
51+
# Mongo Explorer plugin
52+
.idea/**/mongoSettings.xml
53+
54+
# File-based project format
55+
*.iws
56+
57+
# IntelliJ
58+
out/
59+
60+
# mpeltonen/sbt-idea plugin
61+
.idea_modules/
62+
63+
# JIRA plugin
64+
atlassian-ide-plugin.xml
65+
66+
# Cursive Clojure plugin
67+
.idea/replstate.xml
68+
69+
# Crashlytics plugin (for Android Studio and IntelliJ)
70+
com_crashlytics_export_strings.xml
71+
crashlytics.properties
72+
crashlytics-build.properties
73+
fabric.properties
74+
75+
# Editor-based Rest Client
76+
.idea/httpRequests
77+
78+
# Android studio 3.1+ serialized cache file
79+
.idea/caches/build_file_checksums.ser
80+
81+
### PyCharm+all Patch ###
82+
# Ignores the whole .idea folder and all .iml files
83+
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
84+
85+
.idea/
86+
87+
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
88+
89+
*.iml
90+
modules.xml
91+
.idea/misc.xml
92+
*.ipr
93+
94+
# Sonarlint plugin
95+
.idea/sonarlint
96+
97+
### Python ###
98+
# Byte-compiled / optimized / DLL files
99+
__pycache__/
100+
*.py[cod]
101+
*$py.class
102+
103+
# C extensions
104+
*.so
105+
106+
# Distribution / packaging
107+
.Python
108+
build/
109+
develop-eggs/
110+
dist/
111+
downloads/
112+
eggs/
113+
.eggs/
114+
lib/
115+
lib64/
116+
parts/
117+
sdist/
118+
var/
119+
wheels/
120+
share/python-wheels/
121+
*.egg-info/
122+
.installed.cfg
123+
*.egg
124+
MANIFEST
125+
126+
# PyInstaller
127+
# Usually these files are written by a python script from a template
128+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
129+
*.manifest
130+
*.spec
131+
132+
# Installer logs
133+
pip-log.txt
134+
pip-delete-this-directory.txt
135+
136+
# Unit test / coverage reports
137+
htmlcov/
138+
.tox/
139+
.nox/
140+
.coverage
141+
.coverage.*
142+
.cache
143+
nosetests.xml
144+
coverage.xml
145+
*.cover
146+
*.py,cover
147+
.hypothesis/
148+
.pytest_cache/
149+
cover/
150+
151+
# Translations
152+
*.mo
153+
*.pot
154+
155+
# Django stuff:
156+
*.log
157+
local_settings.py
158+
db.sqlite3
159+
db.sqlite3-journal
160+
161+
# Flask stuff:
162+
instance/
163+
.webassets-cache
164+
165+
# Scrapy stuff:
166+
.scrapy
167+
168+
# Sphinx documentation
169+
docs/_build/
170+
171+
# PyBuilder
172+
.pybuilder/
173+
target/
174+
175+
# Jupyter Notebook
176+
.ipynb_checkpoints
177+
178+
# IPython
179+
profile_default/
180+
ipython_config.py
181+
182+
# pyenv
183+
# For a library or package, you might want to ignore these files since the code is
184+
# intended to run in multiple environments; otherwise, check them in:
185+
# .python-version
186+
187+
# pipenv
188+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
189+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
190+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
191+
# install all needed dependencies.
192+
#Pipfile.lock
193+
194+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
195+
__pypackages__/
196+
197+
# Celery stuff
198+
celerybeat-schedule
199+
celerybeat.pid
200+
201+
# SageMath parsed files
202+
*.sage.py
203+
204+
# Environments
205+
.env
206+
.venv
207+
env/
208+
venv/
209+
ENV/
210+
env.bak/
211+
venv.bak/
212+
213+
# Spyder project settings
214+
.spyderproject
215+
.spyproject
216+
217+
# Rope project settings
218+
.ropeproject
219+
220+
# mkdocs documentation
221+
/site
222+
223+
# mypy
224+
.mypy_cache/
225+
.dmypy.json
226+
dmypy.json
227+
228+
# Pyre type checker
229+
.pyre/
230+
231+
# pytype static type analyzer
232+
.pytype/
233+
234+
# Cython debug symbols
235+
cython_debug/
236+
237+
### VisualStudioCode ###
238+
.vscode/*
239+
!.vscode/settings.json
240+
!.vscode/tasks.json
241+
!.vscode/launch.json
242+
!.vscode/extensions.json
243+
*.code-workspace
244+
245+
# Local History for Visual Studio Code
246+
.history/
247+
248+
### VisualStudioCode Patch ###
249+
# Ignore all local history of files
250+
.history
251+
.ionide
252+
253+
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Collect Training and Calibration Data for Gaze Tracking
2+
3+
This tool allows collecting gaze data necessary for personal calibration or training of eye-tracking models. It was developed as part of my master's thesis on [eye tracking with a monocular webcam](https://github.com/pperle/gaze-tracking).
4+
The [framework for the full gaze tracking pipeline](https://github.com/pperle/gaze-tracking-pipeline) is also available.
5+
6+
The output is a folder with a CSV file containing the target that the person is looking at in pixels and the file name of the associated webcam image. For good calibration results, it is recommended to take at least 9 calibration images, the more, the better.
7+
8+
## How to run
9+
10+
1. `pip install -r requirements.txt`
11+
2. If necessary, calibrate the camera using the provided interactive script `python calibrate_camera.py`, see [Camera Calibration by OpenCV](https://docs.opencv.org/4.5.3/dc/dbb/tutorial_py_calibration.html).
12+
3. For higher accuracy, it is also advisable to calibrate the position of the screen as described by [Takahashiet al.](https://doi.org/10.2197/ipsjtcva.8.11), which provide an [OpenCV and matlab implementation](https://github.com/computer-vision/takahashi2012cvpr).
13+
4. `python main.py --base_path=./data/p00`
14+
1. This was only tested on Ubuntu 20.10 and Ubuntu 21.04. If you are using macOS or Windows, you might have to supply the monitor parameters manually, e.g., `--monitor_mm=750,420 --monitor_pixels=1920,1080`, and adjust the `TargetOrientation` values in `utils.py`.
15+
5. Look at the screen and press the corresponding arrow key where the letter `E` is pointing at when the letter color changes from blue to orange. Please press the arrow key several times because sometimes OpenCV doesn't register the click the first time.
16+
6. Press the `q` key when the data collection is complete.
17+
18+
![data collection example](./docs/demo.gif)
19+
20+
7. Visualize the recorded data, image by image by running `python visualization.py --base_path=./data/p00`.
21+
22+
![visualization example](./docs/3d_plot.gif)

camera_calibration.py

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import glob
2+
from datetime import datetime
3+
4+
import cv2
5+
import numpy as np
6+
import yaml
7+
8+
from webcam import WebcamSource
9+
10+
11+
def record_video(width: int, height: int, fps: int) -> None:
12+
"""
13+
Create a mp4 video file with `width`x`height` and `fps` frames per second.
14+
Shows a preview of the recording every 5 frames.
15+
16+
:param width: width of the video
17+
:param height: height of the video
18+
:param fps: frames per second
19+
:return: None
20+
"""
21+
22+
source = WebcamSource(width=width, height=height, fps=fps, buffer_size=10)
23+
video_writer = cv2.VideoWriter(f'{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.mp4', cv2.VideoWriter_fourcc(*'MP4V'), fps, (width, height))
24+
for idx, frame in enumerate(source):
25+
video_writer.write(frame)
26+
source.show(frame, only_print=idx % 5 != 0)
27+
28+
29+
def calibration(image_path, every_nth: int = 1, debug: bool = False, chessboard_grid_size=(7, 7)):
30+
"""
31+
Perform camera calibration on the previously collected images.
32+
Creates `calibration_matrix.yaml` with the camera intrinsic matrix and the distortion coefficients.
33+
34+
:param image_path: path to all png images
35+
:param every_nth: only use every n_th image
36+
:param debug: preview the matched chess patterns
37+
:param chessboard_grid_size: size of chess pattern
38+
:return:
39+
"""
40+
41+
x, y = chessboard_grid_size
42+
43+
# termination criteria
44+
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
45+
46+
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
47+
objp = np.zeros((y * x, 3), np.float32)
48+
objp[:, :2] = np.mgrid[0:x, 0:y].T.reshape(-1, 2)
49+
50+
# Arrays to store object points and image points from all the images.
51+
objpoints = [] # 3d point in real world space
52+
imgpoints = [] # 2d points in image plane.
53+
54+
images = glob.glob(f'{image_path}/*.png')[::every_nth]
55+
56+
found = 0
57+
for fname in images:
58+
img = cv2.imread(fname) # Capture frame-by-frame
59+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
60+
61+
# Find the chess board corners
62+
ret, corners = cv2.findChessboardCorners(gray, (x, y), None)
63+
64+
# If found, add object points, image points (after refining them)
65+
if ret == True:
66+
objpoints.append(objp) # Certainly, every loop objp is the same, in 3D.
67+
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
68+
imgpoints.append(corners2)
69+
70+
found += 1
71+
72+
if debug:
73+
# Draw and display the corners
74+
img = cv2.drawChessboardCorners(img, chessboard_grid_size, corners2, ret)
75+
cv2.imshow('img', img)
76+
cv2.waitKey(100)
77+
78+
print("Number of images used for calibration: ", found)
79+
80+
# When everything done, release the capture
81+
cv2.destroyAllWindows()
82+
83+
# calibration
84+
rms, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
85+
print('rms', rms)
86+
87+
# transform the matrix and distortion coefficients to writable lists
88+
data = {
89+
'rms': np.asarray(rms).tolist(),
90+
'camera_matrix': np.asarray(mtx).tolist(),
91+
'dist_coeff': np.asarray(dist).tolist()
92+
}
93+
94+
# and save it to a file
95+
with open("calibration_matrix.yaml", "w") as f:
96+
yaml.dump(data, f)
97+
98+
print(data)
99+
100+
101+
if __name__ == '__main__':
102+
# 1. record video
103+
record_video(width=1280, height=720, fps=30)
104+
# 2. split video into frames e.g. `ffmpeg -i 2021-10-15_10:30:00.mp4 -f image2 frames/video_01-%07d.png` and delete blurry images
105+
# 3. run calibration on images
106+
calibration('./frames', 30, debug=True)

docs/3d_plot.gif

69 KB
Loading

docs/demo.gif

207 KB
Loading

0 commit comments

Comments
 (0)