Repository: pupil-labs/pye3d-detector (language: python)

Issue:
Apply corneal-refraction correction to `diameter_3d` result
Previously, the corneal-refraction correction was only applied to the `circle_3d` field of the result. The `diameter_3d` value was not corrected for corneal-refraction effects. This was unintended and has been corrected in this PR.
Integration tests
---
[ClickUp Task](https://app.clickup.com/t/jbbezt)

Project uncorrected pupil circle
pye3d's results will use the **_un_corrected** pupil circle for projections into the image plane as it corresponds to what one sees in the image. pye3d will use refraction **corrected** pupil circle for 3d data, including phi/theta.


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..6a34e662
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[run]
+omit =
+	# leading `*/` for pytest-dev/pytest-cov#456
+	*/.tox/*
+
+[report]
+show_missing = True
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..b8aeea17
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = tab
+indent_size = 4
+insert_final_newline = true
+end_of_line = lf
+
+[*.py]
+indent_style = space
+max_line_length = 88
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..89ff3396
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "daily"
+    allow:
+      - dependency-type: "all"
diff --git a/.github/workflows/build-pye3d.yml b/.github/workflows/build-pye3d.yml
index 5a02fe61..889a053f 100644
--- a/.github/workflows/build-pye3d.yml
+++ b/.github/workflows/build-pye3d.yml
@@ -374,9 +374,23 @@ jobs:
           name: distribution
           path: dist/
 
+  build_docs:
+    name: Ensure documentation builds correctly
+    if: github.event_name != 'schedule'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v1
+        with:
+          python-version: 3.9
+      - name: Build docs via tox
+        run: |
+          pip install tox
+          tox -e docs
+
   publish:
     runs-on: ubuntu-latest
-    needs: [build_wheels, build_sdist]
+    needs: [build_wheels, build_sdist, build_docs]
     if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')
     steps:
       - uses: actions/checkout@v2
diff --git a/.gitignore b/.gitignore
index 23be93bf..1c621f62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,157 @@
-########################################################################################
-# This is based on the recommended gitignore for Python, taken from
-# https://github.com/github/gitignore/blob/994f99fc353f523dfe5633067805a1dd4a53040f/Python.gitignore
-# Please add custom ignore rules to the bottom!
-########################################################################################
+version.py
+
+# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux,pycharm
+# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,macos,windows,linux,pycharm
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### PyCharm ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### PyCharm Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
 
 # Byte-compiled / optimized / DLL files
 __pycache__/
@@ -114,6 +263,7 @@ celerybeat.pid
 # Environments
 .env
 .venv
+.conda-env/
 env/
 venv/
 ENV/
@@ -218,3 +368,49 @@ Temporary Items
 # End of https://www.toptal.com/developers/gitignore/api/macos
 
 *.mp4
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+# Support for Project snippet scope
+!.vscode/*.code-snippets
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux,pycharm
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 39a40484..2b47a42a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -15,8 +15,8 @@ repos:
     rev: v2.31.0
     hooks:
       - id: pyupgrade
-        name: PyUpgrade 3.7+
-        args: ["--py37-plus"]
+        name: PyUpgrade 3.6+
+        args: ["--py36-plus"]
         exclude: ^bin/
 
   - repo: https://github.com/pycqa/flake8
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 00000000..251bde62
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,4 @@
+version: 2
+
+conda:
+  environment: conda-env.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index fcea9a07..00000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Changelog
-
-## 0.3.0 (2021-11-10)
-- Change refraction model serialization from pickle to msgpack - [#38](https://github.com/pupil-labs/pye3d-detector/pull/38)
-
-## 0.2.0 (2021-10-12)
-- Lower 3d search confidence results by 40% - [#36](https://github.com/pupil-labs/pye3d-detector/pull/36)
-
-## 0.1.1 (2021-07-07)
-- Extracted source install instructions into `INSTALL_SOURCE.md`
-- Added `Codestyle: Black` badge to `READEME.md`
-- Fixed Github Action badge in `READEME.md` indicating status of the build pipeline
-
-## 0.1.0 (2021-07-07)
-- Use long-term-model pupil-circle normal for gaze direction when frozen - [#31](https://github.com/pupil-labs/pye3d-detector/pull/31)
-- Set `model_confidence` to 0.1 if parameter is out of physiological range - [#35](https://github.com/pupil-labs/pye3d-detector/pull/35)
-- Improve integration tests - [#33](https://github.com/pupil-labs/pye3d-detector/pull/33), [#34](https://github.com/pupil-labs/pye3d-detector/pull/34)
-
-## 0.0.8 (2021-06-17)
-- Automated Python wheels for Python 3.6 - 3.9 on Linux, macOS, and Windows
-
-## 0.0.7 (2021-05-11)
-- Simplification of `Conic` parameter calculation - [#26](https://github.com/pupil-labs/pye3d-detector/pull/26)
-- Incremental performance improvements - [#27](https://github.com/pupil-labs/pye3d-detector/pull/27)
-- Correctly apply corneal-refraction correction to `diameter_3d` result - [#28](https://github.com/pupil-labs/pye3d-detector/pull/28)
-
-## 0.0.6 (2021-03-09)
-
-- Expose 3d eye model RMS fitting residuals; disable them by default - [#24](https://github.com/pupil-labs/pye3d-detector/pull/24)
-
-## 0.0.5 (2021-02-09)
-
-- Pin `scikit-learn==0.24.1` version and update refraction models to that version -
-Avoids warning when unpickling refraction pipeline
-- Replace all occurances of `np.float` with `float` - `np.float` has been
-[deprecated](https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations).
-- Stop treating all warnings in `pye3d/cpp/projections.pyx:unproject_ellipse()` as
-errors - The previous behavior was introduced to avoid repeated division-by-zero errors.
-The new implementation continues to handle these as errors but excludes other types of
-warnings, e.g. `DeprecationWarning`, from this handling.
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 00000000..17710baf
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,57 @@
+0.3.1 (WIP)
+##################
+
+- Correctly calculate direction of ``Observation``'s ``gaze_3d_pair`` lines - #47
+- Cleanup build and transition to `Pupil Labs Python module skeleton
+  <https://github.com/pupil-labs/python-module-skeleton>`_ structure - #44 and #49
+- Add :ref:`examples` - #45
+
+
+0.3.0 (2021-11-10)
+##################
+
+- Change refraction model serialization from pickle to msgpack - #38
+
+0.2.0 (2021-10-12)
+##################
+- Lower 3d search confidence results by 40% - #36
+
+0.1.1 (2021-07-07)
+##################
+
+- Extracted source install instructions into ``INSTALL_SOURCE.md``
+- Added ``Codestyle: Black`` badge to ``READEME.md``
+- Fixed Github Action badge in ``READEME.md`` indicating status of the build pipeline
+
+0.1.0 (2021-07-07)
+##################
+- Use long-term-model pupil-circle normal for gaze direction when frozen - #31
+- Set ``model_confidence`` to 0.1 if parameter is out of physiological range - #35
+- Improve integration tests - #33, #34
+
+0.0.8 (2021-06-17)
+##################
+- Automated Python wheels for Python 3.6 - 3.9 on Linux, macOS, and Windows
+
+0.0.7 (2021-05-11)
+##################
+- Simplification of ``Conic`` parameter calculation - #26
+- Incremental performance improvements - #27
+- Correctly apply corneal-refraction correction to ``diameter_3d`` result - #28
+
+0.0.6 (2021-03-09)
+##################
+
+- Expose 3d eye model RMS fitting residuals; disable them by default - #24
+
+0.0.5 (2021-02-09)
+##################
+
+- Pin ``scikit-learn==0.24.1`` version and update refraction models to that version -
+  Avoids warning when unpickling refraction pipeline
+- Replace all occurances of ``np.float`` with ``float`` - ``np.float`` has been
+  `deprecated <https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations>`_.
+- Stop treating all warnings in `pye3d/cpp/projections.pyx:unproject_ellipse()` as
+  errors - The previous behavior was introduced to avoid repeated division-by-zero errors.
+  The new implementation continues to handle these as errors but excludes other types of
+  warnings, e.g. ``DeprecationWarning``, from this handling.
diff --git a/INSTALL_SOURCE.md b/INSTALL_SOURCE.md
deleted file mode 100644
index b8a5ae8d..00000000
--- a/INSTALL_SOURCE.md
+++ /dev/null
@@ -1,252 +0,0 @@
-# Build and install from source
-
-Follow these instructions if you want to install a modified version of the source code.
-Otherwise, we recommend installing the pre-packaged binary wheels from PyPI:
-
-```
-pip install pye3d
-```
-
-## Build Dependencies
-
-You can skip this step if you have OpenCV and Eigen3 compiled and installed on your computer.
-Scroll to the bottom and continue with installing pye3d.
-
-### Windows
-
-Building the dependencies on Windows requires running the commands in [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/overview). Requires `git` and `cmake` to be in your system PATH. Please run all three install steps in the same shell or redefine `OpenCV_DIR` and `Eigen3_DIR` before running the last step (building and installing pye3d).
-
-#### Build and install OpenCV
-
-```powershell
-# Download OpenCV
-Invoke-WebRequest "https://github.com/opencv/opencv/archive/4.2.0.zip" -OutFile opencv.zip
-
-# Prepare build
-Expand-Archive opencv.zip
-mv opencv/opencv-4.*/* opencv/
-mkdir opencv/build
-
-# Enter build path
-cd opencv/build
-
-# Configure build
-cmake .. -A x64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="." -DBUILD_LIST="core,highgui,videoio,imgcodecs,imgproc,video" -DBUILD_opencv_world=ON -DBUILD_EXAMPLES=OFF -DBUILD_DOCS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DBUILD_opencv_java=OFF -DBUILD_opencv_python=OFF -DWITH_OPENMP=ON -DWITH_IPP=ON -DWITH_CSTRIPES=ON -DWITH_OPENCL=ON -DWITH_CUDA=OFF -DWITH_TBB=OFF -DWITH_MSMF=OFF
-
-# Compile
-cmake --build . --target INSTALL --config Release --parallel
-
-# Define OpenCV location for third step
-$Env:OpenCV_DIR = (pwd)
-
-# Exit build path
-cd ../..
-```
-
-#### Build and install Eigen
-
-```powershell
-# Download Eigen
-Invoke-WebRequest "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip" -OutFile eigen.zip
-
-# Prepare build
-Expand-Archive eigen.zip
-mv eigen/eigen-3.*/* eigen/
-mkdir eigen/build
-
-# Enter build path
-cd eigen/build
-
-# Configure build
-cmake .. -A x64 -DCMAKE_INSTALL_PREFIX="."
-
-# Compile
-cmake --build . --target INSTALL --config Release --parallel
-
-# Define Eigen3 location for third step
-$Env:Eigen3_DIR = (pwd)
-
-# Exit build path
-cd ../..
-```
-
-Scroll to the bottom for the last step.
-
-### Ubuntu
-
-#### Build and install OpenCV
-
-```bash
-# Download OpenCV
-wget -O opencv.zip "https://github.com/opencv/opencv/archive/4.2.0.zip"
-
-# Prepare build
-unzip opencv.zip
-mv opencv-4.* opencv/
-mkdir opencv/build
-
-# Enter build path
-cd opencv/build
-
-# Configure build
-cmake .. \
-    -DCMAKE_BUILD_TYPE=Release \
-    -DCMAKE_INSTALL_PREFIX="." \
-    -DBUILD_LIST="core,highgui,videoio,imgcodecs,imgproc,video" \
-    -DBUILD_opencv_world=ON \
-    -DBUILD_EXAMPLES=OFF \
-    -DBUILD_DOCS=OFF \
-    -DBUILD_PERF_TESTS=OFF \
-    -DBUILD_TESTS=OFF \
-    -DBUILD_opencv_java=OFF \
-    -DBUILD_opencv_python=OFF \
-    -DWITH_OPENMP=ON \
-    -DWITH_IPP=ON \
-    -DWITH_CSTRIPES=ON \
-    -DWITH_OPENCL=ON \
-    -DWITH_CUDA=OFF \
-    -DWITH_TBB=OFF \
-    -DWITH_MSMF=OFF
-
-# Compile
-make
-make install
-
-# Define OpenCV location for third step
-OpenCV_DIR=${pwd}
-
-# Exit build path
-cd ../..
-```
-
-#### Build and install Eigen
-
-```bash
-# Download Eigen
-wget -O eigen.zip "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip"
-
-# Prepare build
-unzip eigen.zip
-mv eigen-3.* eigen/
-mkdir eigen/build
-
-# Enter build path
-cd eigen/build
-
-# Configure build
-cmake .. \
-    -DCMAKE_INSTALL_PREFIX="."
-
-# Compile
-make
-make install
-
-# Define Eigen3 location for third step
-Eigen3_DIR=${pwd}
-
-# Exit build path
-cd ../..
-```
-
-Scroll to the bottom for the last step.
-
-### macOS
-
-Downlaoding the dependencies requires `wget`, which can be installed on macOS with [Homebrew](https://brew.sh):
-```bash
-brew install wget
-```
-
-#### Build and install OpenCV
-
-```bash
-# Download OpenCV
-wget -O opencv.zip "https://github.com/opencv/opencv/archive/4.2.0.zip"
-
-# Prepare build
-unzip opencv.zip
-mv opencv-4.* opencv/
-mkdir opencv/build
-
-# Enter build path
-cd opencv/build
-
-# Configure build
-cmake .. \
-    -DCMAKE_BUILD_TYPE=Release \
-    -DCMAKE_INSTALL_PREFIX="." \
-    -DBUILD_LIST="core,highgui,videoio,imgcodecs,imgproc,video" \
-    -DBUILD_opencv_world=ON \
-    -DBUILD_EXAMPLES=OFF \
-    -DBUILD_DOCS=OFF \
-    -DBUILD_PERF_TESTS=OFF \
-    -DBUILD_TESTS=OFF \
-    -DBUILD_opencv_java=OFF \
-    -DBUILD_opencv_python=OFF \
-    -DWITH_OPENMP=ON \
-    -DWITH_IPP=ON \
-    -DWITH_CSTRIPES=ON \
-    -DWITH_OPENCL=ON \
-    -DWITH_CUDA=OFF \
-    -DWITH_TBB=OFF \
-    -DWITH_MSMF=OFF
-
-# Compile
-make
-make install
-
-# Define OpenCV location for third step
-OpenCV_DIR=${pwd}
-
-# Exit build path
-cd ../..
-```
-
-#### Build and install Eigen
-
-```bash
-# Download Eigen
-wget -O eigen.zip "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip"
-
-# Prepare build
-unzip eigen.zip
-mv eigen-3.* eigen/
-mkdir eigen/build
-
-# Enter build path
-cd eigen/build
-
-# Configure build
-cmake .. \
-    -DCMAKE_INSTALL_PREFIX="."
-
-# Compile
-make
-make install
-
-# Define Eigen3 location for third step
-Eigen3_DIR=${pwd}
-
-# Exit build path
-cd ../..
-```
-
-
-## Build and install `pye3d` (all platforms)
-
-Requires `OpenCV_DIR` and `Eigen3_DIR` environmental variables pointing to the appropriate install locations.
-
-```powershell
-# Download pye3d code
-git clone https://github.com/pupil-labs/pye3d-detector.git
-
-# Prepare build
-cd pye3d-detector
-# optional: activate Python virtual environment here
-
-# Make sure pip is up-to-date
-python -m pip install -U pip
-
-# Build and install pye3d
-python -m pip install .
-```
diff --git a/MANIFEST.in b/MANIFEST.in
index 1acb1bfa..16db254b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,11 +1,12 @@
 # include all required files
-include CMakeLists.txt LICENSE MANIFEST.in pyproject.toml README.md setup.py INSTALL_SOURCE.md
-recursive-include pye3d *
-recursive-include tests *
+include LICENSE MANIFEST.in pyproject.toml setup.py setup.cfg
+recursive-include . *.rst
+recursive-include pye3d/cpp/ *.h CMakeLists.txt
 
 # exclude build artifacts
 global-exclude *.dll *.so *.dylib
 
 # exclude other distribution/install artifacts
-recursive-exclude **/*.egg-info *
-recursive-exclude **/__pycache__ *
+global-exclude .DS_Store
+prune ./.*/
+prune **/__pycache__
diff --git a/README.md b/README.md
deleted file mode 100644
index de5286b1..00000000
--- a/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
-[![Build pye3d](https://github.com/pupil-labs/pye3d-detector/actions/workflows/build-pye3d.yml/badge.svg)](https://github.com/pupil-labs/pye3d-detector/actions/workflows/build-pye3d.yml)
-[![PyPI version](https://badge.fury.io/py/pye3d.svg)](https://badge.fury.io/py/pye3d)
-
-# pye3d
-
-`pye3d` implements a published mathematical 3D eye model capturing ocular kinematics and optics (see [Swirski and Dodgson, 2013](https://www.researchgate.net/publication/264658852_A_fully-automatic_temporal_approach_to_single_camera_glint-free_3D_eye_model_fitting "Swirski and Dodgson, 2013"), as well as [Dierkes, Kassner, and Bulling, 2019](https://www.researchgate.net/publication/333490770_A_fast_approach_to_refraction-aware_eye-model_fitting_and_gaze_prediction "Dierkes, Kassner, and Bulling, 2019")).
-
-## Installation
-We recommend installing the pre-packaged binary wheels from PyPI:
-```
-pip install pye3d
-```
-
-If you want to install a modified version of the source code, you will have to install
-the platform-specific dependencies first. For details, see [`INSTALL_SOURCE.md`](INSTALL_SOURCE.md).
-
-
-## Usage
-
-Here's a quick example of how to pass 2D pupil detection results to `pye3d` (requires standalone
-[2D pupil detector](https://github.com/pupil-labs/pupil-detectors/) installation)
-
-```python
-import cv2
-from pupil_detectors import Detector2D
-from pye3d.detector_3d import CameraModel, DetectorMode, Detector3D
-
-# create 2D detector
-detector_2d = Detector2D()
-
-# create pye3D detector
-camera = CameraModel(focal_length=561.5, resolution=[400, 400])
-detector_3d = Detector3D(camera=camera, long_term_mode=DetectorMode.blocking)
-
-# load eye video
-eye_video = cv2.VideoCapture("eye*.mp4")
-
-# read each frame of video and run pupil detectors
-while eye_video.isOpened():
-    frame_number = eye_video.get(cv2.CAP_PROP_POS_FRAMES)
-    fps = eye_video.get(cv2.CAP_PROP_FPS)
-    ret, eye_frame = eye_video.read()
-
-    if ret:
-        # read video frame as numpy array
-        grayscale_array = cv2.cvtColor(eye_frame, cv2.COLOR_BGR2GRAY)
-
-        # run 2D detector on video frame
-        result_2d = detector_2d.detect(grayscale_array)
-        result_2d["timestamp"] = frame_number / fps
-
-        # pass 2D detection result to 3D detector
-        result_3d = detector_3d.update_and_detect(result_2d, grayscale_array)
-        ellipse_3d = result_3d["ellipse"]
-
-        # draw 3D detection result on eye frame
-        cv2.ellipse(
-            eye_frame,
-            tuple(int(v) for v in ellipse_3d["center"]),
-            tuple(int(v / 2) for v in ellipse_3d["axes"]),
-            ellipse_3d["angle"],
-            0,
-            360,  # start/end angle for drawing
-            (0, 255, 0),  # color (BGR): red
-        )
-
-        # show frame
-        cv2.imshow("eye_frame", eye_frame)
-
-        # press esc to exit
-        if cv2.waitKey(1) & 0xFF == 27:
-            break
-    else:
-        break
-
-eye_video.release()
-cv2.destroyAllWindows()
-
-```
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..cf0e179c
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,45 @@
+.. image:: https://img.shields.io/pypi/v/pye3d.svg
+   :target: `PyPI link`_
+
+.. image:: https://img.shields.io/pypi/pyversions/pye3d.svg
+   :target: `PyPI link`_
+
+.. _PyPI link: https://pypi.org/project/pye3d
+
+.. image:: https://github.com/pupil-labs/pye3d-detector/actions/workflows/build-pye3d.yml/badge.svg
+   :target: https://github.com/pupil-labs/pye3d-detector/actions/workflows/build-pye3d.yml
+   :alt: Build pye3d
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+   :alt: Code style: Black
+
+.. image:: https://readthedocs.org/projects/pye3d-detector/badge/?version=latest
+   :target: https://pye3d-detector.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2021-informational
+   :target: https://blog.jaraco.com/skeleton
+
+
+pye3d Detector
+##############
+
+``pye3d`` implements a published mathematical 3D eye model capturing ocular kinematics
+and optics (see `Swirski and Dodgson, 2013`_, as well as `Dierkes, Kassner, and Bulling, 2019`_.
+
+.. _Swirski and Dodgson, 2013: https://www.researchgate.net/publication/264658852_A_fully-automatic_temporal_approach_to_single_camera_glint-free_3D_eye_model_fitting
+.. _Dierkes, Kassner, and Bulling, 2019: https://www.researchgate.net/publication/333490770_A_fast_approach_to_refraction-aware_eye-model_fitting_and_gaze_prediction
+
+Installation
+------------
+
+We recommend installing the pre-packaged binary wheels from PyPI:
+
+.. code-block:: console
+
+    pip install pye3d
+
+If you want to install a modified version of the source code, you will have to install
+the platform-specific dependencies first. For details, see :ref:`install_from_source`.
+
+For more information see the `documentation <https://pye3d-detector.readthedocs.io>`_.
diff --git a/conda-env.yml b/conda-env.yml
new file mode 100644
index 00000000..e400ffca
--- /dev/null
+++ b/conda-env.yml
@@ -0,0 +1,9 @@
+name: pye3d-conda
+channels:
+  - conda-forge
+dependencies:
+  - python=3.9
+  - eigen
+  - opencv
+  - pip:
+    - -e ".[docs,testing]"
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..59b6af62
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker']
+
+master_doc = "index"
+
+link_files = {
+    '../CHANGES.rst': dict(
+        using=dict(GH='https://github.com'),
+        replace=[
+            dict(
+                pattern=r'(Issue #|\B#)(?P<issue>\d+)',
+                url='{package_url}/issues/{issue}',
+            ),
+            dict(
+                pattern=r'(?m:^((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n)',
+                with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n',
+            ),
+            dict(
+                pattern=r'PEP[- ](?P<pep_number>\d+)',
+                url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/',
+            ),
+        ],
+    )
+}
+
+# Be strict about any broken references:
+nitpicky = True
+
+# Include Python intersphinx mapping to prevent failures
+# jaraco/skeleton#51
+extensions += ['sphinx.ext.intersphinx']
+intersphinx_mapping = {
+    'python': ('https://docs.python.org/3', None),
+}
+html_theme = "furo"
diff --git a/docs/examples.rst b/docs/examples.rst
new file mode 100644
index 00000000..22c6b617
--- /dev/null
+++ b/docs/examples.rst
@@ -0,0 +1,18 @@
+.. _examples:
+
+#############
+Usage example
+#############
+
+Here's a quick example of how to pass 2D pupil detection results to pye3d (requires
+standalone `2D pupil detector <https://github.com/pupil-labs/pupil-detectors/>`_
+installation). Alternatively, you can install the dependencies together with ``pye3d``
+
+.. code-block:: console
+
+   pip install pye3d[examples]
+
+.. literalinclude:: ../examples/process_eye_video.py
+   :language: python
+   :linenos:
+   :emphasize-lines: 11,13,14,26,27,29
diff --git a/docs/history.rst b/docs/history.rst
new file mode 100644
index 00000000..8e217503
--- /dev/null
+++ b/docs/history.rst
@@ -0,0 +1,8 @@
+:tocdepth: 2
+
+.. _changes:
+
+History
+*******
+
+.. include:: ../CHANGES (links).rst
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..17148c4f
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,24 @@
+Welcome to the |project| documentation!
+=======================================
+
+.. include:: ../README.rst
+   :start-line: 26
+   :end-line: 44
+
+Table of Contents
+-----------------
+
+.. toctree::
+   :maxdepth: 1
+
+   install_from_source
+   examples
+   history
+
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/install_from_source.rst b/docs/install_from_source.rst
new file mode 100644
index 00000000..822fa3fe
--- /dev/null
+++ b/docs/install_from_source.rst
@@ -0,0 +1,274 @@
+.. _install_from_source:
+
+#############################
+Build and install from source
+#############################
+
+Follow these instructions if you want to install a modified version of the source code.
+Otherwise, we recommend installing the pre-packaged binary wheels from PyPI:
+
+.. code-block:: console
+
+   pip install pye3d
+
+
+Build Dependencies
+##################
+
+You can skip this step if you have OpenCV and Eigen3 compiled and installed on your computer.
+Scroll to the bottom and continue with installing pye3d.
+
+Windows
+-------
+
+Building the dependencies on Windows requires running the commands in [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/overview). Requires `git` and `cmake` to be in your system PATH. Please run all three install steps in the same shell or redefine `OpenCV_DIR` and `Eigen3_DIR` before running the last step (building and installing pye3d).
+
+Build and install OpenCV
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: powershell
+
+   # Download OpenCV
+   Invoke-WebRequest "https://github.com/opencv/opencv/archive/4.2.0.zip" -OutFile opencv.zip
+
+   # Prepare build
+   Expand-Archive opencv.zip
+   mv opencv/opencv-4.*/* opencv/
+   mkdir opencv/build
+
+   # Enter build path
+   cd opencv/build
+
+   # Configure build
+   cmake .. -A x64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="." -DBUILD_LIST="core,highgui,videoio,imgcodecs,imgproc,video" -DBUILD_opencv_world=ON -DBUILD_EXAMPLES=OFF -DBUILD_DOCS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DBUILD_opencv_java=OFF -DBUILD_opencv_python=OFF -DWITH_OPENMP=ON -DWITH_IPP=ON -DWITH_CSTRIPES=ON -DWITH_OPENCL=ON -DWITH_CUDA=OFF -DWITH_TBB=OFF -DWITH_MSMF=OFF
+
+   # Compile
+   cmake --build . --target INSTALL --config Release --parallel
+
+   # Define OpenCV location for third step
+   $Env:OpenCV_DIR = (pwd)
+
+   # Exit build path
+   cd ../..
+
+Build and install Eigen
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: powershell
+
+   # Download Eigen
+   Invoke-WebRequest "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip" -OutFile eigen.zip
+
+   # Prepare build
+   Expand-Archive eigen.zip
+   mv eigen/eigen-3.*/* eigen/
+   mkdir eigen/build
+
+   # Enter build path
+   cd eigen/build
+
+   # Configure build
+   cmake .. -A x64 -DCMAKE_INSTALL_PREFIX="."
+
+   # Compile
+   cmake --build . --target INSTALL --config Release --parallel
+
+   # Define Eigen3 location for third step
+   $Env:Eigen3_DIR = (pwd)
+
+   # Exit build path
+   cd ../..
+
+Go to :ref:`pyed_source_build_install` for the last step.
+
+Ubuntu
+------
+
+Build and install OpenCV
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: bash
+
+   # Download OpenCV
+   wget -O opencv.zip "https://github.com/opencv/opencv/archive/4.2.0.zip"
+
+   # Prepare build
+   unzip opencv.zip
+   mv opencv-4.* opencv/
+   mkdir opencv/build
+
+   # Enter build path
+   cd opencv/build
+
+   # Configure build
+   cmake .. \
+      -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_INSTALL_PREFIX="." \
+      -DBUILD_LIST="core,highgui,videoio,imgcodecs,imgproc,video" \
+      -DBUILD_opencv_world=ON \
+      -DBUILD_EXAMPLES=OFF \
+      -DBUILD_DOCS=OFF \
+      -DBUILD_PERF_TESTS=OFF \
+      -DBUILD_TESTS=OFF \
+      -DBUILD_opencv_java=OFF \
+      -DBUILD_opencv_python=OFF \
+      -DWITH_OPENMP=ON \
+      -DWITH_IPP=ON \
+      -DWITH_CSTRIPES=ON \
+      -DWITH_OPENCL=ON \
+      -DWITH_CUDA=OFF \
+      -DWITH_TBB=OFF \
+      -DWITH_MSMF=OFF
+
+   # Compile
+   make
+   make install
+
+   # Define OpenCV location for third step
+   OpenCV_DIR=${pwd}
+
+   # Exit build path
+   cd ../..
+
+Build and install Eigen
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: bash
+
+   # Download Eigen
+   wget -O eigen.zip "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip"
+
+   # Prepare build
+   unzip eigen.zip
+   mv eigen-3.* eigen/
+   mkdir eigen/build
+
+   # Enter build path
+   cd eigen/build
+
+   # Configure build
+   cmake .. \
+      -DCMAKE_INSTALL_PREFIX="."
+
+   # Compile
+   make
+   make install
+
+   # Define Eigen3 location for third step
+   Eigen3_DIR=${pwd}
+
+   # Exit build path
+   cd ../..
+
+Go to :ref:`pyed_source_build_install` for the last step.
+
+macOS
+-----
+
+Downlaoding the dependencies requires ``wget``, which can be installed on macOS with
+`Homebrew <https://brew.sh>`_:
+
+.. code-block:: bash
+
+   brew install wget
+
+Build and install OpenCV
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: bash
+
+   # Download OpenCV
+   wget -O opencv.zip "https://github.com/opencv/opencv/archive/4.2.0.zip"
+
+   # Prepare build
+   unzip opencv.zip
+   mv opencv-4.* opencv/
+   mkdir opencv/build
+
+   # Enter build path
+   cd opencv/build
+
+   # Configure build
+   cmake .. \
+      -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_INSTALL_PREFIX="." \
+      -DBUILD_LIST="core,highgui,videoio,imgcodecs,imgproc,video" \
+      -DBUILD_opencv_world=ON \
+      -DBUILD_EXAMPLES=OFF \
+      -DBUILD_DOCS=OFF \
+      -DBUILD_PERF_TESTS=OFF \
+      -DBUILD_TESTS=OFF \
+      -DBUILD_opencv_java=OFF \
+      -DBUILD_opencv_python=OFF \
+      -DWITH_OPENMP=ON \
+      -DWITH_IPP=ON \
+      -DWITH_CSTRIPES=ON \
+      -DWITH_OPENCL=ON \
+      -DWITH_CUDA=OFF \
+      -DWITH_TBB=OFF \
+      -DWITH_MSMF=OFF
+
+   # Compile
+   make
+   make install
+
+   # Define OpenCV location for third step
+   OpenCV_DIR=${pwd}
+
+   # Exit build path
+   cd ../..
+
+Build and install Eigen
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: bash
+
+   # Download Eigen
+   wget -O eigen.zip "https://gitlab.com/libeigen/eigen/-/archive/3.3.7/eigen-3.3.7.zip"
+
+   # Prepare build
+   unzip eigen.zip
+   mv eigen-3.* eigen/
+   mkdir eigen/build
+
+   # Enter build path
+   cd eigen/build
+
+   # Configure build
+   cmake .. \
+      -DCMAKE_INSTALL_PREFIX="."
+
+   # Compile
+   make
+   make install
+
+   # Define Eigen3 location for third step
+   Eigen3_DIR=${pwd}
+
+   # Exit build path
+   cd ../..
+
+See below for the last step.
+
+.. _pyed_source_build_install:
+
+Build and install ``pye3d`` (all platforms)
+###########################################
+
+Requires ``OpenCV_DIR`` and ``Eigen3_DIR`` environmental variables pointing to the
+appropriate install locations.
+
+.. code-block:: bash
+
+   # Download pye3d code
+   git clone https://github.com/pupil-labs/pye3d-detector.git
+
+   # Prepare build
+   cd pye3d-detector
+   # optional: activate Python virtual environment here
+
+   # Make sure pip is up-to-date
+   python -m pip install -U pip
+
+   # Build and install pye3d
+   python -m pip install .
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 00000000..976ba029
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,2 @@
+[mypy]
+ignore_missing_imports = True
diff --git a/pye3d/__init__.py b/pye3d/__init__.py
index 493f7415..aca0166a 100644
--- a/pye3d/__init__.py
+++ b/pye3d/__init__.py
@@ -1,1 +1,12 @@
-__version__ = "0.3.0"
+try:
+    from importlib.metadata import PackageNotFoundError, version
+except ImportError:
+    from importlib_metadata import PackageNotFoundError, version
+
+try:
+    __version__ = version("pye3d")
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+__all__ = ["__version__"]
diff --git a/pyproject.toml b/pyproject.toml
index 4e7f9ddf..c9b3d2f4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,20 @@
 [build-system]
 requires = [
-    "setuptools",
+    "setuptools>=56",
     "wheel",
     "scikit-build",
     "cmake",
-    "ninja",
+    "ninja; platform_system!='Windows'",
     "numpy",
     "cython",
+    "setuptools_scm[toml]>=3.4.1"
 ]
 build-backend = "setuptools.build_meta"
+
+[tool.black]
+skip-string-normalization = true
+
+[tool.setuptools_scm]
+
+[pytest.enabler.cov]
+addopts = "--cov"
diff --git a/tests/generate_ground_truth.py b/scripts/generate_ground_truth.py
similarity index 96%
rename from tests/generate_ground_truth.py
rename to scripts/generate_ground_truth.py
index 7d4d851b..d860598c 100644
--- a/tests/generate_ground_truth.py
+++ b/scripts/generate_ground_truth.py
@@ -1,3 +1,11 @@
+"""
+Generates ground truth data for ``../tests/integration/test_synthetic_metrics.py``
+
+- Depends on ``plsd`` - Pupil Labs internal library
+- Output should be uploaded to
+  https://github.com/pupil-labs/pye3d-detector/wiki/files/pye3d_test_input.npz
+"""
+
 import argparse
 import re as regex
 import typing as T
@@ -281,7 +289,7 @@ def parse_resolution(resolution):
     parser.add_argument("-fps", type=float, default=200)
     parser.add_argument("-N", "--num-samples", type=int, default=1000)
     parser.add_argument(
-        "-o", "--output", default="integration/input/pye3d_test_input.npz"
+        "-o", "--output", default="../tests/integration/input/pye3d_test_input.npz"
     )
     args = parser.parse_args()
 
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..f84c538e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,65 @@
+[metadata]
+name = pye3d
+description = 3D eye model
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+url = https://github.com/pupil-labs/pye3d-detector
+author = Pupil Labs GmbH
+author_email = info@pupil-labs.com
+license_file = LICENSE
+classifiers =
+    Development Status :: 5 - Production/Stable
+    License :: OSI Approved :: MIT License
+    License :: Other/Proprietary License
+    Natural Language :: English
+    Operating System :: MacOS
+    Operating System :: Microsoft :: Windows
+    Operating System :: POSIX :: Linux
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3 :: Only
+    Programming Language :: Python :: 3.6
+    Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
+    Topic :: Scientific/Engineering
+
+[options]
+install_requires =
+    msgpack>=1.*
+    numpy
+    sortedcontainers
+    importlib-metadata;python_version<"3.8"
+python_requires = >=3.6
+include_package_data = true
+
+[options.packages.find]
+exclude =
+    build*
+    dist*
+    docs*
+    tests*
+
+[options.extras_require]
+docs =
+    furo
+    jaraco.packaging>=8.2
+    rst.linker>=1.9
+    sphinx<4.4  # 4.4 does not detect TypeVars correctly
+examples =
+    opencv-python
+    pupil-detectors
+legacy_sklearn_models =
+    joblib
+    scikit-learn
+testing =
+    matplotlib
+    opencv-python-headless
+    pandas
+    pytest>=6
+    pytest-checkdocs>=2.4
+    pytest-cov
+    pytest-enabler>=1.0.1
+    scikit-image
+with_opencv =
+    opencv-python
diff --git a/setup.py b/setup.py
index 3e6faf87..ea51b149 100644
--- a/setup.py
+++ b/setup.py
@@ -1,82 +1,27 @@
-"""
-(*)~---------------------------------------------------------------------------
-Pupil - eye tracking platform
-Copyright (C) 2012-2019 Pupil Labs
+#!/usr/bin/env python
 
-Distributed under the terms of the GNU
-Lesser General Public License (LGPL v3.0).
-See COPYING and COPYING.LESSER for license details.
----------------------------------------------------------------------------~(*)
-"""
 import os
 import platform
-from pathlib import Path
 
 from setuptools import find_packages
 from skbuild import setup
 
-here = Path(__file__).parent
-
-package = "pye3d"
-cpp_dir = f"{package}/cpp"
-
-
-with open(here / "README.md") as f:
-    long_description = f.read()
-
-requirements = [
-    "numpy",
-    "msgpack>=1.*",
-    "sortedcontainers",
-]
-extras_require = {
-    "dev": ["pytest", "tox", "bump2version"],
-    "testing": ["opencv-python-headless", "matplotlib", "pandas", "scikit-image"],
-    "with-opencv": ["opencv-python"],
-    "legacy-sklearn-models": ["joblib", "scikit-learn"],
-    "examples": ["opencv-python", "pupil-detectors"],
-}
-
+cpp_dir = "pye3d/cpp"
 cmake_args = []
+
 if os.environ.get("CI", "false") == "true" and platform.system() == "Windows":
     # The Ninja cmake generator will use mingw (gcc) on windows travis instances, but we
     # need to use msvc for compatibility.
     cmake_args.append("-GVisual Studio 17 2022")
 
-setup(
-    author="Pupil Labs",
-    author_email="info@pupil-labs.com",
-    classifiers=[
-        "Development Status :: 3 - Alpha",
-        "Intended Audience :: Developers",
-        "Natural Language :: English",
-        "Operating System :: MacOS",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX :: Linux",
-        "Programming Language :: C++",
-        "Programming Language :: Cython",
-        "Programming Language :: Python :: 3",
-        "Topic :: Scientific/Engineering",
-        "Topic :: Software Development",
-    ],
-    cmake_args=cmake_args,
-    cmake_install_dir=cpp_dir,
-    cmake_source_dir=cpp_dir,
-    description="3D eye model",
-    extras_require=extras_require,
-    install_requires=requirements,
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    project_urls={
-        "Changelog": (
-            "https://github.com/pupil-labs/pye3d-detector/blob/master/CHANGELOG.md"
-        ),
-        "Pupil Core Documentation": "https://docs.pupil-labs.com/core/",
-        "Pupil Labs Homepage": "https://pupil-labs.com/",
-    },
-    name=package,
-    package_data={package: ["refraction_models/*.msgpack"]},
-    packages=find_packages(),
-    url="https://github.com/pupil-labs/pye3d-detector/",
-    version="0.3.0",
-)
+
+if __name__ == "__main__":
+    setup(
+        # `packages` cannot be defined in setup.cfg, otherwise the C extensions will
+        # not be copied to the correct place
+        packages=find_packages(),
+        cmake_args=cmake_args,
+        cmake_install_dir=cpp_dir,
+        cmake_source_dir=cpp_dir,
+        package_data={"pye3d": ["refraction_models/*.msgpack"]},
+    )
diff --git a/tox.ini b/tox.ini
index 1fbd1b0b..ccd23c1f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,22 +1,46 @@
 [tox]
-envlist = py36, py37, py38, py39
-skip_missing_interpreters = true
-isolated_build = true
+requires = tox-conda
+envlist = python
+minversion = 3.2
+# https://github.com/jaraco/skeleton/issues/6
+tox_pip_extensions_ext_venv_update = true
+toxworkdir={env:TOX_WORK_DIR:.tox}
+isolated_build=true
 
 [testenv]
 deps = pytest
 commands =
 	pytest tests {posargs}
-# usedevelop = True
-extras = testing
 passenv =
-    OpenCV_DIR
-    ARCHFLAGS
+	OpenCV_DIR
+	Eigen3_DIR
+	ARCHFLAGS
+extras = testing
+conda_env = conda-env.yml
+skip_install = True
+
+[testenv:docs]
+extras =
+	docs
+	testing
+changedir = docs
+commands =
+	python -m sphinx -W --keep-going . {toxinidir}/build/html
+skip_install = True
 
-[gh-actions]
-python =
-    3.6: py36
-    3.7: py37
-    3.8: py38
-    3.9: py39
-    3.10: py10
+[testenv:release]
+skip_install = True
+deps =
+	build
+	twine>=3
+	jaraco.develop>=7.1
+passenv =
+	TWINE_PASSWORD
+	GITHUB_TOKEN
+setenv =
+	TWINE_USERNAME = {env:TWINE_USERNAME:__token__}
+commands =
+	python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)"
+	python -m build
+	python -m twine upload dist/*
+	python -m jaraco.develop.create-github-release


Test patch:
diff --git a/pytest.ini b/pytest.ini
index 74389058..512f3202 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,3 +1,7 @@
 [pytest]
 log_cli=true
 log_level=20
+norecursedirs=dist build .tox .eggs
+addopts=--doctest-modules
+doctest_optionflags=ALLOW_UNICODE ELLIPSIS
+filterwarnings=
diff --git a/tests/test_package.py b/tests/test_package.py
index 4370063d..3ee680f2 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -1,6 +1,10 @@
 import pytest
 
 
+def test_package_metadata() -> None:
+    assert hasattr(pytest, "__version__")
+
+
 def test_import():
     from pye3d.camera import CameraModel
     from pye3d.detector_3d import Detector3D