Repository: robotpy/robotpy-cscore (language: python)

Issue:
Generate pyi files
One of the advantages of moving to robotpy-build would have pyi files automatically. However, robotpy-build and conda would likely not play well.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 3bd7930..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,127 +0,0 @@
----
-name: CI
-on:
-  pull_request:
-  push:
-    branches:
-    - main
-    tags:
-    - '*'
-
-jobs:
-  check:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-      - uses: psf/black@stable
-
-  sdist:
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v1
-        with:
-          submodules: true
-      - uses: actions/setup-python@v2
-      - run: python setup.py sdist
-      - uses: actions/upload-artifact@v2
-        with:
-          name: sdist
-          path: dist
-
-  test:
-    needs: [sdist]
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: [ubuntu-18.04, ubuntu-20.04]
-
-    steps:
-      - uses: actions/download-artifact@v2
-        with:
-          name: sdist
-          path: dist
-
-      - name: Add OpenCV 3.4 PPA
-        if: matrix.os == 'ubuntu-18.04'
-        run: sudo add-apt-repository -y ppa:~timsc/opencv-3.4
-      - name: Use gcc-8
-        uses: robotpy/build-actions/setup-cxx@v2022
-        if: matrix.os == 'ubuntu-18.04'
-
-      - name: Install dependencies
-        run: |
-          sudo apt-get install --no-install-recommends -y \
-            libopencv-dev python3-opencv
-
-      - name: Install Python setup requirements
-        run: python3 -m pip install setuptools wheel pkgconfig
-
-      - run: python3 -m pip install -v dist/*.tar.gz
-      - run: python3 -m cscore --help
-
-  cross-build:
-    runs-on: ubuntu-latest
-    needs: [sdist]
-    container:
-      image: robotpy/roborio-cross-ubuntu:2022.3
-
-    steps:
-    - uses: actions/download-artifact@v2
-      with:
-        name: sdist
-        path: dist
-    - run: tar -xf dist/*.gz --strip-components=1
-    - run: rm dist/*.gz
-    - run: /build/venv/bin/cross-python -m pip install robotpy-wpiutil robotpy-opencv-core-dev distro
-    - run: /build/venv/bin/cross-python setup.py bdist_wheel
-    - uses: actions/upload-artifact@v2
-      with:
-        name: dist-roborio
-        path: dist
-
-  #
-  # Publish wheels to robotpy site, pypi
-  #
-
-  publish-rpyrepo:
-    runs-on: ubuntu-latest
-    needs: [check, test, cross-build]
-    if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
-
-    steps:
-
-    #
-    # RoboRIO wheel
-    #
-
-    - uses: actions/download-artifact@v2
-      with:
-        name: dist-roborio
-        path: dist-roborio/
-
-    - uses: robotpy/github-action-scp@atomic
-      with:
-        local: dist-roborio
-        remote: public_html/2022/roborio
-        host: narya.tortall.net
-        username: ${{ secrets.SSH_USER }}
-        privateKey: ${{ secrets.SSH_KEY }}
-        passphrase: ${{ secrets.SSH_PASSPHRASE }}
-        atomicPut: true
-
-  publish-pypi:
-    runs-on: ubuntu-latest
-    needs: [check, test]
-    if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
-
-    steps:
-      - uses: actions/download-artifact@v2
-        with:
-          name: sdist
-          path: dist
-      - name: Publish to PyPI
-        uses: pypa/gh-action-pypi-publish@v1.4.2
-        with:
-          user: __token__
-          password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml
new file mode 100644
index 0000000..520b2b5
--- /dev/null
+++ b/.github/workflows/dist.yml
@@ -0,0 +1,25 @@
+---
+name: dist
+
+on:
+  pull_request:
+  push:
+    branches:
+    - main
+    tags:
+    - '*'
+
+jobs:
+  ci:
+    uses: robotpy/build-actions/.github/workflows/package-ci.yml@v2023
+    with:
+      enable_raspbian: false
+      enable_sphinx_check: false
+    secrets:
+      SSH_USER: ${{ secrets.SSH_USER }}
+      SSH_KEY: ${{ secrets.SSH_KEY }}
+      SSH_PASSPHRASE: ${{ secrets.SSH_PASSPHRASE }}
+      META_REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
+      RTD_TOKEN: ${{ secrets.RTD_TOKEN }}
+      RTD_WEBHOOK: ${{ secrets.RTD_WEBHOOK }}
+      PYPI_API_TOKEN: ${{ secrets.PYPI_PASSWORD }}
diff --git a/.github/workflows/gen-docs.yml b/.github/workflows/gen-docs.yml
deleted file mode 100644
index 58b653d..0000000
--- a/.github/workflows/gen-docs.yml
+++ /dev/null
@@ -1,32 +0,0 @@
----
-name: Gen C++ docs
-on: [workflow_dispatch]
-
-jobs:
-  build:
-    runs-on: ubuntu-20.04
-    steps:
-      - uses: actions/checkout@v2
-        with:
-          fetch-depth: 0
-          submodules: true
-
-      - name: Install apt dependencies
-        run: |
-          sudo apt-get install --no-install-recommends -y \
-            libopencv-dev python3-opencv
-
-      - name: Install requirements
-        run: python3 -m pip install setuptools wheel pkgconfig -r docs/requirements.txt
-
-      - run: python3 -m pip install -e . --user --no-build-isolation
-
-      - run: python3 docs/gencpp.py
-
-      - name: Commit docs/objects.rst and push
-        run: |
-          git config user.name 'github-actions[bot]'
-          git config user.email action@github.com
-          git add docs/objects.rst
-          git commit -m 'Regenerate _cscore docs'
-          git push
diff --git a/.gitignore b/.gitignore
index 0d768cc..fc4ef09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,13 +4,15 @@ __pycache__
 *.so
 *.dll
 
-/docs/_sidebar.rst.inc
-/docs/_build
 /cscore/_init_cscore.py
 
 dist
 build
 version.py
 
+/cscore/pkgcfg.py
+/cscore/py.typed
+/cscore/_cscore
+
 .project
 .pydevproject
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 7fdf469..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,7 +0,0 @@
-[submodule "cscore_src"]
-	path = cscore_src
-	url = https://github.com/robotpy/cscore.git
-[submodule "pybind11"]
-	path = pybind11
-	url = https://github.com/robotpy/pybind11.git
-	branch = stable
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..c0a2ee1
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,13 @@
+version: 2
+
+sphinx:
+  configuration: docs/conf.py
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
+python:
+  install:
+  - requirements: docs/requirements.txt
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index a4f427f..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,12 +0,0 @@
-include LICENSE
-exclude pyproject.toml
-recursive-include src *.h *.hpp
-recursive-include cscore_src/cscore/src/main/native *.cpp *.h
-exclude cscore_src/cscore/src/main/native/cpp/jni/CameraServerJNI.cpp
-include cscore_src/cscore/src/main/native/include/*.inc
-recursive-include cscore_src/wpiutil/src/main/native *.cpp *.h
-exclude cscore_src/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp
-exclude cscore_src/wpiutil/src/main/native/cpp/PortForwarder.cpp
-recursive-include cscore_src/wpiutil/src/main/native/include *.hpp *.inc
-recursive-include pybind11/include *.h
-include pybind11/LICENSE
diff --git a/README.rst b/README.rst
index 58ce69e..d7eca15 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,7 @@ library used in the FIRST Robotics competition to serve video to/from robots.
 However, the core library is suitable for use in other applications, and is
 comparable to something like mjpg-streamer.
 
-This library has only been tested with Python 3, and will not work with
-Python 2.7. We do not intend to support using Python 2.7.
+This library requires python 3.6+
 
 Documentation
 =============
diff --git a/cscore/__init__.py b/cscore/__init__.py
index a3d7431..7d1f70f 100644
--- a/cscore/__init__.py
+++ b/cscore/__init__.py
@@ -1,8 +1,50 @@
 from . import _init_cscore
-from _cscore import *
+
 from ._logging import enableLogging
 
-from .cameraserver import CameraServer, VideoException
+# autogenerated by 'robotpy-build create-imports cscore'
+from ._cscore import (
+    AxisCamera,
+    CameraServer,
+    CvSink,
+    CvSource,
+    HttpCamera,
+    ImageSink,
+    ImageSource,
+    MjpegServer,
+    RawEvent,
+    UsbCamera,
+    UsbCameraInfo,
+    VideoCamera,
+    VideoEvent,
+    VideoListener,
+    VideoMode,
+    VideoProperty,
+    VideoSink,
+    VideoSource,
+)
+
+__all__ = [
+    "AxisCamera",
+    "CameraServer",
+    "CvSink",
+    "CvSource",
+    "HttpCamera",
+    "ImageSink",
+    "ImageSource",
+    "MjpegServer",
+    "RawEvent",
+    "UsbCamera",
+    "UsbCameraInfo",
+    "VideoCamera",
+    "VideoEvent",
+    "VideoListener",
+    "VideoMode",
+    "VideoProperty",
+    "VideoSink",
+    "VideoSource",
+    "enableLogging",
+]
 
 try:
     from .version import __version__
diff --git a/cscore/__main__.py b/cscore/__main__.py
index 11994e7..1703462 100644
--- a/cscore/__main__.py
+++ b/cscore/__main__.py
@@ -7,8 +7,9 @@
 import stat
 import sys
 import threading
+import time
 
-from networktables import NetworkTables
+from ntcore import NetworkTableInstance
 
 log_datefmt = "%H:%M:%S"
 log_format = "%(asctime)s:%(msecs)03d %(levelname)-8s: %(name)-20s: %(message)s"
@@ -49,6 +50,16 @@ def main():
     nt_server_group.add_argument(
         "--team", help="Team number to specify robot", type=int
     )
+    parser.add_argument(
+        "--nt-protocol",
+        choices=[3, 4],
+        type=int,
+        help="NetworkTables protocol",
+        default=4,
+    )
+    parser.add_argument(
+        "--nt-identity", default="cscore", help="NetworkTables identity"
+    )
     parser.add_argument(
         "vision_py",
         nargs="?",
@@ -70,10 +81,16 @@ def main():
     logger = logging.getLogger("cscore")
 
     # Initialize NetworkTables next
+    ntinst = NetworkTableInstance.getDefault()
     if args.team:
-        NetworkTables.startClientTeam(args.team)
+        ntinst.setServerTeam(args.team)
+    else:
+        ntinst.setServer(args.robot)
+
+    if args.nt_protocol == 3:
+        ntinst.startClient3(args.nt_identity)
     else:
-        NetworkTables.initialize(server=args.robot)
+        ntinst.startClient4(args.nt_identity)
 
     # If stdin is a pipe, then die when the pipe goes away
     # -> this allows us to detect if a parent process exits
@@ -88,11 +105,10 @@ def main():
 
         # If no python file specified, then just start the automatic capture
         if args.vision_py is None:
-            from .cameraserver import CameraServer
+            from ._cscore import CameraServer
 
-            cs = CameraServer.getInstance()
-            cs.startAutomaticCapture()
-            cs.waitForever()
+            CameraServer.startAutomaticCapture()
+            CameraServer.waitForever()
 
         else:
             s = args.vision_py.split(":", 1)
diff --git a/cscore/_info.py b/cscore/_info.py
deleted file mode 100644
index e02099c..0000000
--- a/cscore/_info.py
+++ /dev/null
@@ -1,5 +0,0 @@
-class Info:
-    def module_info(self):
-        from cscore import __version__
-
-        return "cscore", __version__
diff --git a/cscore/_logging.py b/cscore/_logging.py
index 6002634..fe1ead9 100644
--- a/cscore/_logging.py
+++ b/cscore/_logging.py
@@ -1,8 +1,12 @@
 import logging
+import typing
 
-from _cscore import setLogger as _setLogger
+from ._cscore import _setLogger
 
 
-def enableLogging(level=logging.DEBUG):
+def enableLogging(level: typing.Optional[int] = None):
+    """Enable logging for cscore"""
+    if level is None:
+        level = logging.DEBUG
     logger = logging.getLogger("cscore")
     _setLogger(lambda lvl, file, line, msg: logger.log(lvl, msg), level)
diff --git a/cscore/cameraserver.py b/cscore/cameraserver.py
deleted file mode 100644
index 3424f54..0000000
--- a/cscore/cameraserver.py
+++ /dev/null
@@ -1,732 +0,0 @@
-# validated: 2019-02-15 DS d55ca191b848 edu/wpi/first/cameraserver/CameraServer.java
-# ----------------------------------------------------------------------------
-# Copyright (c) 2016-2019 FIRST. All Rights Reserved.
-# Open Source Software - may be modified and shared by FRC teams. The code
-# must be accompanied by the FIRST BSD license file in the root directory of
-# the project.
-# ----------------------------------------------------------------------------
-
-import socket
-import threading
-import warnings
-from typing import Dict, List, Optional, Sequence, Union, overload
-
-import _cscore as cscore
-from _cscore import VideoEvent, VideoMode, VideoProperty, VideoSink, VideoSource
-from ._logging import enableLogging
-
-import logging
-
-logger = logging.getLogger("cscore.cserver")
-
-from networktables import NetworkTables
-
-__all__ = ["CameraServer", "VideoException"]
-
-
-class VideoException(Exception):
-    pass
-
-
-class CameraServer:
-    """Singleton class for creating and keeping camera servers.
-
-    This is a higher level wrapper around the cscore functionality, and also
-    publishes camera information to NetworkTables so that dashboards can easily
-    find and display the camera streams.
-    """
-
-    kBasePort = 1181
-    kPublishName = "/CameraPublisher"
-
-    @classmethod
-    def getInstance(cls) -> "CameraServer":
-        """Get the CameraServer instance."""
-        try:
-            return cls._server
-        except AttributeError:
-            cls._server = cls()
-            return cls._server
-
-    @staticmethod
-    def enableLogging(level=logging.INFO):
-        enableLogging(level=level)
-
-    # python-specific helper
-    def _getSourceTable(self, source):
-        return self._tables.get(source.getHandle())
-
-    @staticmethod
-    def _makeSourceValue(source: VideoSource) -> str:
-        kind = source.getKind()
-        if kind == VideoSource.Kind.kUsb:
-            return "usb:" + cscore.getUsbCameraPath(source.getHandle())
-
-        elif kind == VideoSource.Kind.kHttp:
-            urls = cscore.getHttpCameraUrls(source.getHandle())
-            if urls:
-                return "ip:" + urls[0]
-            else:
-                return "ip:"
-
-        elif kind == VideoSource.Kind.kCv:
-            return "cv:"
-
-        else:
-            return "unknown:"
-
-    @staticmethod
-    def _makeStreamValue(address: str, port: int) -> str:
-        return "mjpg:http://%s:%d/?action=stream" % (address, port)
-
-    def _getSinkStreamValues(self, sink: VideoSink) -> List[str]:
-        with self._mutex:
-            # Ignore all but MjpegServer
-            if sink.getKind() != VideoSink.Kind.kMjpeg:
-                return []
-
-            # Get port
-            port = sink.getPort()
-
-            # Generate values
-            values = []
-            listenAddress = sink.getListenAddress()
-            if listenAddress:
-                # If a listen address is specified, only use that
-                values.append(self._makeStreamValue(listenAddress, port))
-            else:
-                # Otherwise generate for hostname and all interface addresses
-                values.append(
-                    self._makeStreamValue(socket.gethostname() + ".local", port)
-                )
-                for addr in self._addresses:
-                    if addr == "127.0.0.1":
-                        continue  # ignore localhost
-
-                    values.append(self._makeStreamValue(addr, port))
-
-            return values
-
-    def _getSourceStreamValues(self, source: VideoSource) -> List[str]:
-        with self._mutex:
-            # Ignore all but HttpCamera
-            if source.getKind() != VideoSource.Kind.kHttp:
-                return []
-
-            sourceHandle = source.getHandle()
-
-            # Generate values
-            values = ["mjpg:%s" % v for v in cscore.getHttpCameraUrls(sourceHandle)]
-
-            # Look to see if we have a passthrough server for this source
-            for sink in self._sinks.values():
-                sinkSourceHandle = sink.getSource().getHandle()
-                if (
-                    sourceHandle == sinkSourceHandle
-                    and sink.getKind() == VideoSink.Kind.kMjpeg
-                ):
-                    # Add USB-only passthrough
-                    port = sink.getPort()
-                    values.append(self._makeStreamValue("172.22.11.2", port))
-                    break
-
-            return values
-
-    def _updateStreamValues(self) -> None:
-        with self._mutex:
-            # Over all the sinks...
-            for sink in self._sinks.values():
-
-                # Get the source's subtable (if none exists, we're done)
-                fixedSource = self._fixedSources.get(sink.getHandle())
-                if fixedSource:
-                    source = fixedSource
-                else:
-                    source = sink.getSource()
-                sourceHandle = source.getHandle()
-                if not sourceHandle:
-                    continue
-
-                table = self._getSourceTable(source)
-                if table is not None:
-                    # Don't set stream values if this is a HttpCamera passthrough
-                    if source.getKind() == VideoSource.Kind.kHttp:
-                        continue
-
-                    # Set table value
-                    values = self._getSinkStreamValues(sink)
-                    if values:
-                        table.getEntry("streams").setStringArray(values)
-
-            # Over all the sources...
-            for source in self._sources.values():
-                # Get the source's subtable (if none exists, we're done)
-                table = self._getSourceTable(source)
-                if table is not None:
-                    # Set table value
-                    values = self._getSourceStreamValues(source)
-                    if values:
-                        table.getEntry("streams").setStringArray(values)
-
-    _pixelFormats = {
-        cscore.VideoMode.PixelFormat.kMJPEG: "MJPEG",
-        cscore.VideoMode.PixelFormat.kYUYV: "YUYV",
-        cscore.VideoMode.PixelFormat.kRGB565: "RGB565",
-        cscore.VideoMode.PixelFormat.kBGR: "BGR",
-        cscore.VideoMode.PixelFormat.kGray: "Gray",
-    }
-
-    @classmethod
-    def _pixelFormatToString(cls, pixelFormat: VideoMode.PixelFormat) -> str:
-        return cls._pixelFormats.get(pixelFormat, "Unknown")
-
-    @classmethod
-    def _videoModeToString(cls, mode: VideoMode) -> str:
-        """Provide string description of video mode.
-
-        The returned string is "{width}x{height} {format} {fps} fps".
-        """
-        return "%sx%s %s %s fps" % (
-            mode.width,
-            mode.height,
-            cls._pixelFormatToString(mode.pixelFormat),
-            mode.fps,
-        )
-
-    @classmethod
-    def _getSourceModeValues(cls, source: VideoSource) -> List[str]:
-        modes = source.enumerateVideoModes()
-        return [cls._videoModeToString(mode) for mode in modes]
-
-    @staticmethod
-    def _putSourcePropertyValue(table, event: VideoEvent, isNew: bool) -> None:
-        if event.name.startswith("raw_"):
-            name = "RawProperty/" + event.name
-            infoName = "RawPropertyInfo/" + event.name
-        else:
-            name = "Property/" + event.name
-            infoName = "PropertyInfo/" + event.name
-
-        entry = table.getEntry(name)
-        prop = event.getProperty()
-        propertyKind = prop.getKind()
-
-        if propertyKind == VideoProperty.Kind.kBoolean:
-            if isNew:
-                entry.setDefaultBoolean(event.value != 0)
-            else:
-                entry.setBoolean(event.value != 0)
-
-        elif propertyKind in (VideoProperty.Kind.kInteger, VideoProperty.Kind.kEnum):
-            if isNew:
-                entry.setDefaultDouble(event.value)
-                table.getEntry(infoName + "/min").setDouble(prop.getMin())
-                table.getEntry(infoName + "/max").setDouble(prop.getMax())
-                table.getEntry(infoName + "/step").setDouble(prop.getStep())
-                table.getEntry(infoName + "/default").setDouble(prop.getDefault())
-            else:
-                entry.setDouble(event.value)
-
-        elif propertyKind == VideoProperty.Kind.kString:
-            if isNew:
-                entry.setDefaultString(name, event.valueStr)
-            else:
-                entry.setString(event.valueStr)
-
-    def __init__(self):
-        self._mutex = threading.RLock()
-
-        self._defaultUsbDevice = 0  # note: atomic upstream, keep accesses thread-safe
-        self._primarySourceName = None  # type: Optional[str]
-
-        self._sources = {}  # type: Dict[str, VideoSource]
-        self._sinks = {}  # type: Dict[str, VideoSink]
-        self._tables = {}  # type: Dict[int, networktables.NetworkTable]
-        # source handle indexed by sink handle
-        self._fixedSources = {}  # type: Dict[int, cscore.CvSource]
-        self._publishTable = NetworkTables.getTable(self.kPublishName)
-        self._nextPort = self.kBasePort
-        self._addresses = []
-
-        # We publish sources to NetworkTables using the following structure:
-        # "/CameraPublisher/{Source.Name/" - root
-        # - "source" (string): Descriptive, prefixed with type (e.g. "usb:0")
-        # - "streams" (string array): URLs that can be used to stream data
-        # - "description" (string): Description of the source
-        # - "connected" (boolean): Whether source is connected
-        # - "mode" (string): Current video mode
-        # - "modes" (string array): Available video modes
-        # - "Property/{Property}" - Property values
-        # - "PropertyInfo/{Property}" - Property supporting information
-
-        # Listener for video events
-        def _ve(e):
-            try:
-                self._onVideoEvent(e)
-            except Exception:
-                logger.exception("Unhandled exception in _onVideoEvent")
-
-        self._videoListener = cscore.VideoListener(_ve, 0x4FFF, True)
-
-        # Listener for NetworkTable events
-        # .. figures this uses the one API we don't really support
-        self._nt = NetworkTables.getGlobalTable()
-        ntapi = getattr(NetworkTables, "_api", NetworkTables)
-        ntapi.addEntryListener(
-            self.kPublishName + "/",
-            self._onTableChange,
-            NetworkTables.NotifyFlags.IMMEDIATE | NetworkTables.NotifyFlags.UPDATE,
-        )
-
-    def _onVideoEvent(self, event: VideoEvent) -> None:
-        source = event.getSource()
-
-        if event.kind == VideoEvent.Kind.kSourceCreated:
-            # Create subtable for the camera
-            table = self._publishTable.getSubTable(event.name)
-
-            self._tables[source.getHandle()] = table
-
-            table.getEntry("source").setString(self._makeSourceValue(source))
-            table.getEntry("description").setString(source.getDescription())
-            table.getEntry("connected").setBoolean(source.isConnected())
-            table.getEntry("streams").setStringArray(
-                self._getSourceStreamValues(source)
-            )
-
-            try:
-                mode = source.getVideoMode()  # type: cscore.VideoMode
-                table.getEntry("mode").setDefaultString(self._videoModeToString(mode))
-                table.getEntry("modes").setStringArray(
-                    self._getSourceModeValues(source)
-                )
-            except VideoException:
-                # Do nothing. Let the other event handlers update this if there is an error.
-                pass
-
-        elif event.kind == VideoEvent.Kind.kSourceDestroyed:
-            table = self._getSourceTable(source)
-            if table is not None:
-                table.getEntry("source").setString("")
-                table.getEntry("streams").setStringArray([])
-                table.getEntry("modes").setStringArray([])
-
-        elif event.kind == VideoEvent.Kind.kSourceConnected:
-            table = self._getSourceTable(source)
-            if table is not None:
-                # update the description too (as it may have changed)
-                table.getEntry("description").setString(source.getDescription())
-                table.getEntry("connected").setBoolean(True)
-
-        elif event.kind == VideoEvent.Kind.kSourceDisconnected:
-            table = self._getSourceTable(source)
-            if table is not None:
-                table.getEntry("connected").setBoolean(False)
-
-        elif event.kind == VideoEvent.Kind.kSourceVideoModesUpdated:
-            table = self._getSourceTable(source)
-            if table is not None:
-                table.getEntry("modes").setStringArray(
-                    self._getSourceModeValues(source)
-                )
-
-        elif event.kind == VideoEvent.Kind.kSourceVideoModeChanged:
-            table = self._getSourceTable(source)
-            if table is not None:
-                table.getEntry("mode").setString(self._videoModeToString(event.mode))
-
-        elif event.kind == VideoEvent.Kind.kSourcePropertyCreated:
-            table = self._getSourceTable(source)
-            if table is not None:
-                self._putSourcePropertyValue(table, event, True)
-
-        elif event.kind == VideoEvent.Kind.kSourcePropertyValueUpdated:
-            table = self._getSourceTable(source)
-            if table is not None:
-                self._putSourcePropertyValue(table, event, False)
-
-        elif event.kind == VideoEvent.Kind.kSourcePropertyChoicesUpdated:
-            table = self._getSourceTable(source)
-            if table is not None:
-                prop = event.getProperty()
-                choices = prop.getChoices()  # type: List[str]
-                table.getEntry(
-                    "PropertyInfo/" + event.name + "/choices"
-                ).setStringArray(choices)
-
-        elif event.kind in (
-            VideoEvent.Kind.kSinkSourceChanged,
-            VideoEvent.Kind.kSinkCreated,
-            VideoEvent.Kind.kSinkDestroyed,
-            VideoEvent.Kind.kNetworkInterfacesChanged,
-        ):
-            self._addresses = cscore.getNetworkInterfaces()
-            self._updateStreamValues()
-
-    def _onTableChange(self, event) -> None:
-        key = event.name  # type: str
-        relativeKey = key[len(self.kPublishName) + 1 :]
-
-        # remove leading '/'
-        key = event.name[1:]
-
-        # get source (sourceName/...)
-        subKeyIndex = relativeKey.find("/")
-        if subKeyIndex == -1:
-            return
-
-        sourceName = relativeKey[:subKeyIndex]
-        source = self._sources.get(sourceName)  # type: Optional[VideoSource]
-        if source is None:
-            return
-
-        # get subkey
-        relativeKey = relativeKey[subKeyIndex + 1 :]
-
-        # handle standard names
-        if relativeKey == "mode":
-            # Reset to current mode
-            self._nt.putString(key, self._videoModeToString(source.getVideoMode()))
-            return
-        elif relativeKey.startswith("Property/"):
-            propName = relativeKey[9:]
-        elif relativeKey.startswith("RawProperty/"):
-            propName = relativeKey[12:]
-        else:
-            return  # ignore
-
-        # everything else is a property
-        # .. reset to current setting
-        prop = source.getProperty(propName)  # type: VideoProperty
-        if prop.isBoolean():
-            self._nt.putBoolean(key, prop.get() != 0)
-        elif prop.isInteger() or prop.isEnum():
-            self._nt.putNumber(key, prop.get())
-        elif prop.isString():
-            self._nt.putString(key, prop.getString())
-
-    # TODO: remove (*, return_server=True) -> MjpegServer
-    @overload
-    def startAutomaticCapture(
-        self, *, return_server: bool = False
-    ) -> Union[cscore.UsbCamera, cscore.MjpegServer]:
-        ...
-
-    @overload
-    def startAutomaticCapture(
-        self, *, dev: int, name: Optional[str] = None, return_server: bool = False
-    ) -> Union[cscore.UsbCamera, cscore.MjpegServer]:
-        ...
-
-    @overload
-    def startAutomaticCapture(
-        self, *, name: str, path: str, return_server: bool = False
-    ) -> Union[cscore.UsbCamera, cscore.MjpegServer]:
-        ...
-
-    # TODO: startAutomaticCapture(self, *, camera: VideoSource) -> MjpegServer
-    @overload
-    def startAutomaticCapture(
-        self, *, camera: VideoSource, return_server: bool = False
-    ) -> Union[VideoSource, cscore.MjpegServer]:
-        ...
-
-    def startAutomaticCapture(
-        self, *, dev=None, name=None, path=None, camera=None, return_server=None
-    ):
-        """Start automatically capturing images to send to the dashboard.
-
-        You should call this method to see a camera feed on the dashboard.
-        If you also want to perform vision processing on the roboRIO, use
-        :meth:`getVideo` to get access to the camera images.
-
-        :param dev: If specified, the device number to use
-        :param name: If specified, the name to use for the camera (dev must be specified)
-        :param path: If specified, device path (e.g. "/dev/video0") of the camera
-        :param camera: If specified, an existing camera object to use
-        :param return_server: If specified, return the server instead of the camera
-
-        :returns: USB Camera object, or the camera argument, or the created server
-        :rtype: VideoSource or MjpegServer
-
-        The following argument combinations are accepted -- all arguments must be specified
-        as keyword arguments:
-
-        * (no args)
-        * dev
-        * dev, name
-        * name, path
-        * camera
-
-        The first time this is called with no arguments, a USB Camera from
-        device 0 is created.  Subsequent calls increment the device number
-        (e.g. 1, 2, etc).
-
-        .. note:: USB Cameras are not available on all platforms. If it is not
-                  available on your platform, :exc:`.VideoException` is thrown
-        """
-        if return_server is None:
-            return_server = camera is not None
-        else:
-            warnings.warn(
-                "startAutomaticCapture(return_server=True) is deprecated (default if passed a camera object)",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-
-        if camera is not None:
-            assert dev is None and name is None and path is None
-        else:
-            if not hasattr(cscore, "UsbCamera"):
-                raise VideoException(
-                    "USB Camera support not available on this platform!"
-                )
-
-            if dev is not None:
-                assert path is None
-                arg = dev
-
-            elif path is not None:
-                assert name is not None
-                arg = path
-
-            else:
-                # Note: this get-and-increment should be atomic.
-                with self._mutex:
-                    arg = self._defaultUsbDevice
-                    self._defaultUsbDevice += 1
-
-            if name is None:
-                name = "USB Camera %d" % arg
-
-            camera = cscore.UsbCamera(name, arg)
-
-        self.addCamera(camera)
-        server = self.addServer(name="serve_" + camera.getName())
-        server.setSource(camera)
-
-        if return_server:
-            return server
-        else:
-            return camera
-
-    def addAxisCamera(
-        self, host: Union[str, Sequence[str]], name: str = "Axis Camera"
-    ) -> cscore.AxisCamera:
-        """Adds an Axis IP camera.
-
-        :param host: Camera host IP/DNS name or list of camera host IPs/DNS names
-        :param name: The name to give the camera (optional)
-
-        :returns: Axis camera object
-        """
-        camera = cscore.AxisCamera(name, host)
-        # Create a passthrough MJPEG server for USB access
-        self.startAutomaticCapture(camera=camera)
-        return camera
-
-    def addSwitchedCamera(self, name: str) -> cscore.MjpegServer:
-        """Adds a virtual camera for switching between two streams.  Unlike the
-        other addCamera methods, this returns a VideoSink rather than a
-        VideoSource.  Calling setSource() on the returned object can be used
-        to switch the actual source of the stream.
-
-        :param name: Name of camera
-
-        :returns: Server object
-        """
-        source = cscore.CvSource(name, VideoMode.PixelFormat.kMJPEG, 160, 120, 30)
-        server = self.startAutomaticCapture(camera=source)
-        self._fixedSources[server.getHandle()] = source
-        return server
-
-    @overload
-    def getVideo(self) -> cscore.CvSink:
-        ...
-
-    @overload
-    def getVideo(self, *, name: str) -> cscore.CvSink:
-        ...
-
-    @overload
-    def getVideo(self, *, camera: VideoSource) -> cscore.CvSink:
-        ...
-
-    def getVideo(self, *, name=None, camera=None) -> cscore.CvSink:
-        """Get OpenCV access to specified camera. This allows you to
-        get images from the camera for image processing.
-
-        :param name: Name of camera to retrieve video for
-        :param camera: Camera object
-
-        :returns: CvSink object corresponding to camera
-
-        All arguments must be specified as keyword arguments. The following
-        combinations are permitted:
-
-        * (no args)
-        * name
-        * camera
-
-        If there are no arguments, then this will retrieve access to the
-        primary camera. No arguments will fail if a camera feed has not already
-        been added via :meth:`startAutomaticCapture` or :meth:`addCamera`
-
-        """
-        with self._mutex:
-            if camera is not None:
-                assert name is None
-
-            else:
-                if name is None:
-                    name = self._primarySourceName
-
-                camera = self._sources.get(name)
-
-            if camera is None:
-                raise VideoException("no camera available")
-
-            name = "opencv_" + camera.getName()
-
-            sink = self._sinks.get(name)  # type: Optional[VideoSink]
-            if sink is not None:
-                kind = sink.getKind()
-                if kind != VideoSink.Kind.kCv:
-                    raise VideoException(
-                        "expected OpenCV sink, but got %s (name: %s)" % (kind, name)
-                    )
-
-                return sink
-
-        newsink = cscore.CvSink(name)
-        newsink.setSource(camera)
-
-        with self._mutex:
-            self._sinks[name] = newsink
-
-        return newsink
-
-    def putVideo(self, name: str, width: int, height: int) -> cscore.CvSource:
-        """Create a MJPEG stream with OpenCV input. This can be called to pass custom
-        annotated images to the dashboard.
-
-        :param name: Name to give the stream
-        :param width: Width of the image being sent
-        :param height: Height of the image being sent
-
-        :returns: CvSource object that you can publish images to
-        """
-        source = cscore.CvSource(
-            name, cscore.VideoMode.PixelFormat.kMJPEG, width, height, 30
-        )
-        self.startAutomaticCapture(camera=source)
-        return source
-
-    @overload
-    def addServer(self, *, name: str, port: Optional[int] = None) -> cscore.MjpegServer:
-        ...
-
-    @overload
-    def addServer(self, *, server: VideoSink) -> cscore.MjpegServer:
-        ...
-
-    def addServer(
-        self, *, name=None, port: Optional[int] = None, server=None
-    ) -> cscore.MjpegServer:
-        """Adds a MJPEG server
-
-        :param name: Server name
-        :param port: Port of server (if None, use next available port)
-        :param server: Server
-
-        :returns: server object
-
-        All arguments must be specified as keyword arguments. The following
-        combinations are accepted:
-
-        * name
-        * name, port
-        * server
-
-        """
-
-        with self._mutex:
-            if server is not None:
-                assert name is None and port is None
-
-            else:
-                assert name is not None
-
-                if port is None:
-                    port = self._nextPort
-                    self._nextPort += 1
-
-                server = cscore.MjpegServer(name, port)
-
-            sname = server.getName()
-            sport = server.getPort()
-
-            logger.info("CameraServer '%s' listening on port %s", sname, sport)
-
-            self._sinks[sname] = server
-            return server
-
-    def removeServer(self, name: str) -> None:
-        """Removes a server by name.
-
-        :param name: Server name
-        """
-        with self._mutex:
-            self._sinks.pop(name, None)
-
-    def getServer(self, name: Optional[str] = None) -> VideoSource:
-        """Get server by name, or for the primary camera feed if no name is specified.
-
-        This is only valid to call after a camera feed has been added
-        with :meth:`startAutomaticCapture` or :meth:`addServer`.
-
-        :param name: Server name
-
-        :returns: server object
-        """
-        with self._mutex:
-            if name is None:
-                if self._primarySourceName is None:
-                    raise VideoException("No primary video source defined")
-                name = "serve_" + self._primarySourceName
-
-            server = self._sources.get(name)
-            if server is None:
-                raise VideoException("server %s is not available" % server)
-
-            return server
-
-    def addCamera(self, camera: VideoSource) -> None:
-        """Adds an already created camera.
-
-        :param camera: Camera object
-        """
-        name = camera.getName()
-        with self._mutex:
-            if self._primarySourceName is None:
-                self._primarySourceName = name
-
-            if name in self._sources:
-                raise KeyError("Camera with name '%s' already exists" % name)
-
-            self._sources[name] = camera
-
-    def removeCamera(self, name: str) -> None:
-        """Removes a camera by name.
-
-        :param name: Camera name
-        """
-        with self._mutex:
-            self._sources.pop(name, None)
-
-    def waitForever(self):
-        """Infinitely loops until the process dies"""
-        import time
-
-        while True:
-            time.sleep(1)
diff --git a/cscore/cvnp/README.md b/cscore/cvnp/README.md
new file mode 100644
index 0000000..4aa3684
--- /dev/null
+++ b/cscore/cvnp/README.md
@@ -0,0 +1,11 @@
+This directory comes from https://github.com/pthom/cvnp/tree/master/cvnp
+
+ The MIT License (MIT)
+
+Copyright © 2022 <Pascal Thomet>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/cscore/cvnp/cvnp.cpp b/cscore/cvnp/cvnp.cpp
new file mode 100644
index 0000000..6757a6d
--- /dev/null
+++ b/cscore/cvnp/cvnp.cpp
@@ -0,0 +1,122 @@
+#include "cvnp/cvnp.h"
+
+// Thanks to Dan Mašek who gave me some inspiration here:
+// https://stackoverflow.com/questions/60949451/how-to-send-a-cvmat-to-python-over-shared-memory
+
+namespace cvnp
+{
+
+    namespace detail
+    {
+        namespace py = pybind11;
+        
+        py::dtype determine_np_dtype(int cv_depth)
+        {
+            for (auto format_synonym : cvnp::sTypeSynonyms)
+                if (format_synonym.cv_depth == cv_depth)
+                    return format_synonym.dtype();
+
+            std::string msg = "numpy does not support this OpenCV depth: " + std::to_string(cv_depth) +  " (in determine_np_dtype)";
+            throw std::invalid_argument(msg.c_str());
+        }
+
+        int determine_cv_depth(pybind11::dtype dt)
+        {
+            for (auto format_synonym : cvnp::sTypeSynonyms)
+                if (format_synonym.np_format[0] == dt.char_())
+                    return format_synonym.cv_depth;
+
+            std::string msg = std::string("OpenCV does not support this numpy format: ") + dt.char_() +  " (in determine_np_dtype)";
+            throw std::invalid_argument(msg.c_str());
+        }
+
+        int determine_cv_type(const py::array& a, int depth)
+        {
+            if (a.ndim() < 2)
+                throw std::invalid_argument("determine_cv_type needs at least two dimensions");
+            if (a.ndim() > 3)
+                throw std::invalid_argument("determine_cv_type needs at most three dimensions");
+            if (a.ndim() == 2)
+                return CV_MAKETYPE(depth, 1);
+            //We now know that shape.size() == 3
+            return CV_MAKETYPE(depth, a.shape()[2]);
+        }
+
+        cv::Size determine_cv_size(const py::array& a)
+        {
+            if (a.ndim() < 2)
+                throw std::invalid_argument("determine_cv_size needs at least two dimensions");
+            return cv::Size(static_cast<int>(a.shape()[1]), static_cast<int>(a.shape()[0]));
+        }
+
+        std::vector<std::size_t> determine_shape(const cv::Mat& m)
+        {
+            if (m.channels() == 1) {
+                return {
+                    static_cast<size_t>(m.rows)
+                    , static_cast<size_t>(m.cols)
+                };
+            }
+            return {
+                static_cast<size_t>(m.rows)
+                , static_cast<size_t>(m.cols)
+                , static_cast<size_t>(m.channels())
+            };
+        }
+
+        py::capsule make_capsule_mat(const cv::Mat& m)
+        {
+            return py::capsule(new cv::Mat(m)
+                , [](void *v) { delete reinterpret_cast<cv::Mat*>(v); }
+            );
+        }
+
+
+    } // namespace detail
+
+    pybind11::array mat_to_nparray(const cv::Mat& m, bool share_memory)
+    {
+        if (!m.isContinuous())
+            throw std::invalid_argument("cvnp::mat_to_nparray / Only contiguous Mats supported / You can clone() your matrix to obtain a contiguous copy.");
+        if (share_memory)
+            return pybind11::array(detail::determine_np_dtype(m.depth())
+                , detail::determine_shape(m)
+                , m.data
+                , detail::make_capsule_mat(m)
+                );
+        else
+            return pybind11::array(detail::determine_np_dtype(m.depth())
+                , detail::determine_shape(m)
+                , m.data
+                );
+    }
+
+
+    bool is_array_contiguous(const pybind11::array& a)
+    {
+        pybind11::ssize_t expected_stride = a.itemsize();
+        for (int i = a.ndim() - 1; i >=0; --i)
+        {
+            pybind11::ssize_t current_stride = a.strides()[i];
+            if (current_stride != expected_stride)
+                return false;
+            expected_stride = expected_stride * a.shape()[i];
+        }
+        return true;
+    }
+
+
+    cv::Mat nparray_to_mat(pybind11::array& a)
+    {
+        bool is_contiguous = is_array_contiguous(a);
+        if (! is_contiguous)
+            throw std::invalid_argument("cvnp::nparray_to_mat / Only contiguous numpy arrays are supported. / Please use np.ascontiguousarray() to convert your matrix");
+
+        int depth = detail::determine_cv_depth(a.dtype());
+        int type = detail::determine_cv_type(a, depth);
+        cv::Size size = detail::determine_cv_size(a);
+        cv::Mat m(size, type, a.mutable_data(0));
+        return m;
+    }
+
+} // namespace cvnp
diff --git a/cscore/cvnp/cvnp.h b/cscore/cvnp/cvnp.h
new file mode 100644
index 0000000..d69005d
--- /dev/null
+++ b/cscore/cvnp/cvnp.h
@@ -0,0 +1,371 @@
+#pragma once
+#include "cvnp/cvnp_synonyms.h"
+#include "cvnp/cvnp_shared_mat.h"
+
+#include <opencv2/core/core.hpp>
+#include <pybind11/numpy.h>
+
+#include <iostream>
+#include <stdexcept>
+#include <vector>
+#include <stdlib.h>
+//
+// Explicit transformers between cv::Mat / cv::Matx and numpy.ndarray, with *possibly*/ shared memory
+// Also see automatic casters in the namespace pybind11:detail below
+//
+namespace cvnp
+{
+    //
+    // Public interface
+    //
+    pybind11::array mat_to_nparray(const cv::Mat& m, bool share_memory);
+    cv::Mat         nparray_to_mat(pybind11::array& a);
+
+        template<typename _Tp, int _rows, int _cols>
+    pybind11::array matx_to_nparray(const cv::Matx<_Tp, _rows, _cols>& m, bool share_memory);
+        template<typename _Tp, int _rows, int _cols>
+    void            nparray_to_matx(pybind11::array &a, cv::Matx<_Tp, _rows, _cols>& out_matrix);
+
+
+    //
+    // Private details and implementations below
+    //
+    namespace detail
+    {
+        template<typename _Tp, int _rows, int _cols>
+        pybind11::capsule make_capsule_matx(const cv::Matx<_Tp, _rows, _cols>& m)
+        {
+            return pybind11::capsule(new cv::Matx<_Tp, _rows, _cols>(m)
+                , [](void *v) { delete reinterpret_cast<cv::Matx<_Tp, _rows, _cols>*>(v); }
+            );
+        }
+    } // namespace detail
+
+    template<typename _Tp, int _rows, int _cols>
+    pybind11::array matx_to_nparray(const cv::Matx<_Tp, _rows, _cols>& m, bool share_memory)
+    {
+        if (share_memory)
+            return pybind11::array(
+                pybind11::dtype::of<_Tp>()
+                , std::vector<std::size_t> {_rows, _cols}
+                , m.val
+                , detail::make_capsule_matx<_Tp, _rows, _cols>(m)
+                );
+        else
+            return pybind11::array(
+                pybind11::dtype::of<_Tp>()
+                , std::vector<std::size_t> {_rows, _cols}
+                , m.val
+            );
+    }
+
+    template<typename _Tp, int _rows, int _cols>
+    void nparray_to_matx(pybind11::array &a, cv::Matx<_Tp, _rows, _cols>& out_matrix)
+    {
+        size_t mat_size = (size_t)(_rows * _cols);
+        if (a.size() != mat_size)
+            throw std::runtime_error("Bad size");
+
+        _Tp* arrayValues = (_Tp*) a.data(0);
+        for (size_t i = 0; i < mat_size; ++i)
+            out_matrix.val[i] = arrayValues[i];
+    }
+} // namespace cvnp
+
+
+
+//
+// 1. Casts with shared memory between {cv::Mat, cv::Matx, cv::Vec} and numpy.ndarray
+//
+// 2. Casts without shared memory between {cv::Size, cv::Point, cv::Point3} and python tuples
+//
+namespace pybind11
+{
+    namespace detail
+    {
+        //
+        // Cast between cvnp::Mat_shared and numpy.ndarray
+        // The cast between cv::Mat and numpy.ndarray works
+        //   - *with* shared memory when going from C++ to Python
+        //   - *with* shared memory when going from Python to C++
+        //   any modification to the Matrix size, type, and values is immediately
+        //   impacted on both sides.
+        //
+        template<>
+        struct type_caster<cvnp::Mat_shared>
+        {
+        public:
+        PYBIND11_TYPE_CASTER(cvnp::Mat_shared, _("numpy.ndarray"));
+
+            /**
+             * Conversion part 1 (Python->C++):
+             * Return false upon failure.
+             * The second argument indicates whether implicit conversions should be applied.
+             */
+            bool load(handle src, bool)
+            {
+                if (!isinstance<array>(src)) {
+                    return false;
+                }
+                auto a = reinterpret_borrow<array>(src);
+                auto new_mat = cv::Mat(cvnp::nparray_to_mat(a));
+                value.Value = new_mat;
+                return true;
+            }
+
+            /**
+             * Conversion part 2 (C++ -> Python):
+             * The second and third arguments are used to indicate the return value policy and parent object
+             * (for ``return_value_policy::reference_internal``) and are generally
+             * ignored by implicit casters.
+             */
+            static handle cast(const cvnp::Mat_shared &m, return_value_policy, handle defval)
+            {
+                auto a = cvnp::mat_to_nparray(m.Value, true);
+                return a.release();
+            }
+        };
+
+
+        //
+        // Cast between cv::Mat and numpy.ndarray
+        // The cast between cv::Mat and numpy.ndarray works *without* shared memory.
+        //   - *without* shared memory when going from C++ to Python
+        //   - *with* shared memory when going from Python to C++
+        //
+        template<>
+        struct type_caster<cv::Mat>
+        {
+        public:
+        PYBIND11_TYPE_CASTER(cv::Mat, _("numpy.ndarray"));
+
+            /**
+             * Conversion part 1 (Python->C++):
+             * Return false upon failure.
+             * The second argument indicates whether implicit conversions should be applied.
+             */
+            bool load(handle src, bool)
+            {
+                if (!isinstance<array>(src)) {
+                    return false;
+                }
+                auto a = reinterpret_borrow<array>(src);
+                auto new_mat = cvnp::nparray_to_mat(a);
+                value = new_mat;
+                return true;
+            }
+
+            /**
+             * Conversion part 2 (C++ -> Python):
+             * The second and third arguments are used to indicate the return value policy and parent object
+             * (for ``return_value_policy::reference_internal``) and are generally
+             * ignored by implicit casters.
+             */
+            static handle cast(const cv::Mat &m, return_value_policy, handle defval)
+            {
+                auto a = cvnp::mat_to_nparray(m, false);
+                return a.release();
+            }
+        };
+
+
+        //
+        // Cast between cvnp::Matx_shared<_rows,_cols> (aka Matx33d, Matx21d, etc) + Vec<_rows> (aka Vec1d, Vec2f, etc) and numpy.ndarray
+        // The cast between cvnp::Matx_shared, cvnp::Vec_shared and numpy.ndarray works *with* shared memory:
+        //   any modification to the Matrix size, type, and values is immediately
+        //   impacted on both sides.
+        //   - *with* shared memory when going from C++ to Python
+        //   - *with* shared memory when going from Python to C++
+        //
+        template<typename _Tp, int _rows, int _cols>
+        struct type_caster<cvnp::Matx_shared<_Tp, _rows, _cols> >
+        {
+            using Matshared_xxx = cvnp::Matx_shared<_Tp, _rows, _cols>;
+
+        public:
+        PYBIND11_TYPE_CASTER(Matshared_xxx, _("numpy.ndarray"));
+
+            // Conversion part 1 (Python->C++)
+            bool load(handle src, bool)
+            {
+                if (!isinstance<array>(src)) {
+                    return false;
+                }
+
+                auto a = reinterpret_borrow<array>(src);
+                cvnp::nparray_to_matx<_Tp, _rows, _cols>(a, value.Value);
+                return true;
+            }
+
+            // Conversion part 2 (C++ -> Python)
+            static handle cast(const Matshared_xxx &m, return_value_policy, handle defval)
+            {
+                auto a = cvnp::matx_to_nparray<_Tp, _rows, _cols>(m.Value, true);
+                return a.release();
+            }
+        };
+
+
+        //
+        // Cast between cv::Matx<_rows,_cols> (aka Matx33d, Matx21d, etc) + Vec<_rows> (aka Vec1d, Vec2f, etc) and numpy.ndarray
+        // The cast between cv::Matx, cv::Vec and numpy.ndarray works *without* shared memory.
+        //   - *without* shared memory when going from C++ to Python
+        //   - *with* shared memory when going from Python to C++
+        //
+        template<typename _Tp, int _rows, int _cols>
+        struct type_caster<cv::Matx<_Tp, _rows, _cols> >
+        {
+            using Matxxx = cv::Matx<_Tp, _rows, _cols>;
+
+        public:
+        PYBIND11_TYPE_CASTER(Matxxx, _("numpy.ndarray"));
+
+            // Conversion part 1 (Python->C++)
+            bool load(handle src, bool)
+            {
+                if (!isinstance<array>(src)) {
+                    return false;
+                }
+
+                auto a = reinterpret_borrow<array>(src);
+                cvnp::nparray_to_matx<_Tp, _rows, _cols>(a, value);
+                return true;
+            }
+
+            // Conversion part 2 (C++ -> Python)
+            static handle cast(const Matxxx &m, return_value_policy, handle defval)
+            {
+                auto a = cvnp::matx_to_nparray<_Tp, _rows, _cols>(m, false);
+                return a.release();
+            }
+        };
+
+
+
+        //
+        // Cast between cv::Size and a simple python tuple.
+        // No shared memory, you cannot modify the width or the height without
+        // transferring the whole Size from C++, or the tuple from python
+        //
+        template<typename _Tp>
+        struct type_caster<cv::Size_<_Tp>>
+        {
+            using SizeTp = cv::Size_<_Tp>;
+
+        public:
+        PYBIND11_TYPE_CASTER(SizeTp, _("tuple"));
+
+            // Conversion part 1 (Python->C++, i.e tuple -> Size)
+            bool load(handle src, bool)
+            {
+                if (!isinstance<pybind11::tuple>(src)) {
+                    return false;
+                }
+
+                auto tuple = pybind11::reinterpret_borrow<pybind11::tuple>(src);
+                if (tuple.size() != 2)
+                    throw std::invalid_argument("Size should be in a tuple of size 2");
+
+                SizeTp r;
+                r.width =  tuple[0].cast<_Tp>();
+                r.height = tuple[1].cast<_Tp>();
+
+                value = r;
+                return true;
+            }
+
+            // Conversion part 2 (C++ -> Python, i.e Size -> tuple)
+            static handle cast(const SizeTp &value, return_value_policy, handle defval)
+            {
+                auto result = pybind11::make_tuple(value.width, value.height);
+                return result.release();
+            }
+        };
+
+
+        //
+        // Cast between cv::Point and a simple python tuple
+        // No shared memory, you cannot modify x or y without
+        // transferring the whole Point from C++, or the tuple from python
+        //
+        template<typename _Tp>
+        struct type_caster<cv::Point_<_Tp>>
+        {
+            using PointTp = cv::Point_<_Tp>;
+
+        public:
+        PYBIND11_TYPE_CASTER(PointTp , _("tuple"));
+
+            // Conversion part 1 (Python->C++)
+            bool load(handle src, bool)
+            {
+                if (!isinstance<pybind11::tuple>(src)) {
+                    return false;
+                }
+
+                auto tuple = pybind11::reinterpret_borrow<pybind11::tuple>(src);
+                if (tuple.size() != 2)
+                    throw std::invalid_argument("Point should be in a tuple of size 2");
+
+                PointTp r;
+                r.x = tuple[0].cast<_Tp>();
+                r.y = tuple[1].cast<_Tp>();
+
+                value = r;
+                return true;
+            }
+
+            // Conversion part 2 (C++ -> Python)
+            static handle cast(const PointTp &value, return_value_policy, handle defval)
+            {
+                auto result = pybind11::make_tuple(value.x, value.y);
+                return result.release();
+            }
+        };
+
+
+
+        //
+        // Point3
+        // No shared memory
+        //
+        template<typename _Tp>
+        struct type_caster<cv::Point3_<_Tp>>
+        {
+            using PointTp = cv::Point3_<_Tp>;
+
+        public:
+        PYBIND11_TYPE_CASTER(PointTp , _("tuple"));
+
+            // Conversion part 1 (Python->C++)
+            bool load(handle src, bool)
+            {
+                if (!isinstance<pybind11::tuple>(src)) {
+                    return false;
+                }
+
+                auto tuple = pybind11::reinterpret_borrow<pybind11::tuple>(src);
+                if (tuple.size() != 3)
+                    throw std::invalid_argument("Point3 should be in a tuple of size 3");
+
+                PointTp r;
+                r.x = tuple[0].cast<_Tp>();
+                r.y = tuple[1].cast<_Tp>();
+                r.z = tuple[2].cast<_Tp>();
+
+                value = r;
+                return true;
+            }
+
+            // Conversion part 2 (C++ -> Python)
+            static handle cast(const PointTp &value, return_value_policy, handle defval)
+            {
+                auto result = pybind11::make_tuple(value.x, value.y, value.z);
+                return result.release();
+            }
+        };
+
+
+    }  // namespace detail
+}  // namespace pybind11
+
diff --git a/cscore/cvnp/cvnp_shared_mat.h b/cscore/cvnp/cvnp_shared_mat.h
new file mode 100644
index 0000000..f22d087
--- /dev/null
+++ b/cscore/cvnp/cvnp_shared_mat.h
@@ -0,0 +1,101 @@
+#pragma once
+
+#include <opencv2/core/core.hpp>
+
+
+namespace cvnp
+{
+    //
+    // Wrappers for cv::Matx, cv::Mat and cv::Vec when you explicitly intend to share memory
+    //
+
+    struct Mat_shared
+    {
+        Mat_shared(const cv::Mat value = cv::Mat()): Value(value) {};
+        cv::Mat Value;
+    };
+
+
+    template<typename Tp, int m, int n>
+    struct Matx_shared
+    {
+        Matx_shared(const cv::Matx<Tp, m, n>& value = cv::Matx<Tp, m, n>()): Value(value) {};
+        cv::Matx<Tp, m, n> Value;
+    };
+
+
+    template<typename Tp, int m>
+    struct Vec_shared : public cv::Vec<Tp, m>
+    {
+        Vec_shared(const cv::Vec<Tp, m>& value): Value(value) {};
+        cv::Vec<Tp, m> Value;
+    };
+
+
+    typedef Matx_shared<float, 1, 2> Matx_shared12f;
+    typedef Matx_shared<double, 1, 2> Matx_shared12d;
+    typedef Matx_shared<float, 1, 3> Matx_shared13f;
+    typedef Matx_shared<double, 1, 3> Matx_shared13d;
+    typedef Matx_shared<float, 1, 4> Matx_shared14f;
+    typedef Matx_shared<double, 1, 4> Matx_shared14d;
+    typedef Matx_shared<float, 1, 6> Matx_shared16f;
+    typedef Matx_shared<double, 1, 6> Matx_shared16d;
+
+    typedef Matx_shared<float, 2, 1> Matx_shared21f;
+    typedef Matx_shared<double, 2, 1> Matx_shared21d;
+    typedef Matx_shared<float, 3, 1> Matx_shared31f;
+    typedef Matx_shared<double, 3, 1> Matx_shared31d;
+    typedef Matx_shared<float, 4, 1> Matx_shared41f;
+    typedef Matx_shared<double, 4, 1> Matx_shared41d;
+    typedef Matx_shared<float, 6, 1> Matx_shared61f;
+    typedef Matx_shared<double, 6, 1> Matx_shared61d;
+
+    typedef Matx_shared<float, 2, 2> Matx_shared22f;
+    typedef Matx_shared<double, 2, 2> Matx_shared22d;
+    typedef Matx_shared<float, 2, 3> Matx_shared23f;
+    typedef Matx_shared<double, 2, 3> Matx_shared23d;
+    typedef Matx_shared<float, 3, 2> Matx_shared32f;
+    typedef Matx_shared<double, 3, 2> Matx_shared32d;
+
+    typedef Matx_shared<float, 3, 3> Matx_shared33f;
+    typedef Matx_shared<double, 3, 3> Matx_shared33d;
+
+    typedef Matx_shared<float, 3, 4> Matx_shared34f;
+    typedef Matx_shared<double, 3, 4> Matx_shared34d;
+    typedef Matx_shared<float, 4, 3> Matx_shared43f;
+    typedef Matx_shared<double, 4, 3> Matx_shared43d;
+
+    typedef Matx_shared<float, 4, 4> Matx_shared44f;
+    typedef Matx_shared<double, 4, 4> Matx_shared44d;
+    typedef Matx_shared<float, 6, 6> Matx_shared66f;
+    typedef Matx_shared<double, 6, 6> Matx_shared66d;
+
+
+    typedef Vec_shared<uchar, 2> Vec_shared2b;
+    typedef Vec_shared<uchar, 3> Vec_shared3b;
+    typedef Vec_shared<uchar, 4> Vec_shared4b;
+
+    typedef Vec_shared<short, 2> Vec_shared2s;
+    typedef Vec_shared<short, 3> Vec_shared3s;
+    typedef Vec_shared<short, 4> Vec_shared4s;
+
+    typedef Vec_shared<ushort, 2> Vec_shared2w;
+    typedef Vec_shared<ushort, 3> Vec_shared3w;
+    typedef Vec_shared<ushort, 4> Vec_shared4w;
+
+    typedef Vec_shared<int, 2> Vec_shared2i;
+    typedef Vec_shared<int, 3> Vec_shared3i;
+    typedef Vec_shared<int, 4> Vec_shared4i;
+    typedef Vec_shared<int, 6> Vec_shared6i;
+    typedef Vec_shared<int, 8> Vec_shared8i;
+
+    typedef Vec_shared<float, 2> Vec_shared2f;
+    typedef Vec_shared<float, 3> Vec_shared3f;
+    typedef Vec_shared<float, 4> Vec_shared4f;
+    typedef Vec_shared<float, 6> Vec_shared6f;
+
+    typedef Vec_shared<double, 2> Vec_shared2d;
+    typedef Vec_shared<double, 3> Vec_shared3d;
+    typedef Vec_shared<double, 4> Vec_shared4d;
+    typedef Vec_shared<double, 6> Vec_shared6d;
+} // namespace cvnp
diff --git a/cscore/cvnp/cvnp_synonyms.cpp b/cscore/cvnp/cvnp_synonyms.cpp
new file mode 100644
index 0000000..7ec62cb
--- /dev/null
+++ b/cscore/cvnp/cvnp_synonyms.cpp
@@ -0,0 +1,70 @@
+#include "cvnp/cvnp_synonyms.h"
+#include <string>
+#include <iostream>
+
+
+namespace cvnp
+{
+    std::vector<TypeSynonyms> sTypeSynonyms
+    {
+        { CV_8U,  "CV_8U", pybind11::format_descriptor<uint8_t>::format(),   "np.uint8" },
+        { CV_8S,  "CV_8S", pybind11::format_descriptor<int8_t>::format(),    "np.int8" },
+        { CV_16U, "CV_16U", pybind11::format_descriptor<uint16_t>::format(), "np.uint16" },
+        { CV_16S, "CV_16S", pybind11::format_descriptor<int16_t>::format(),  "np.int16" },
+        { CV_32S, "CV_32S", pybind11::format_descriptor<int32_t>::format(),  "np.int32" },
+        { CV_32F, "CV_32F", pybind11::format_descriptor<float>::format(),    "float" },
+        { CV_64F, "CV_64F", pybind11::format_descriptor<double>::format(),   "np.float64" },
+
+        // Note: this format needs adaptations
+        //#if (CV_MAJOR_VERSION >= 4)
+        //        { CV_16F, "CV_16F", pybind11::format_descriptor<cv::float16_t>::format() },
+        //#endif
+    };
+
+
+    static int sColumnWidth = 12;
+
+    static std::string align_center(const std::string& s)
+    {
+        int nb_spaces = s.size() < sColumnWidth ? sColumnWidth - s.size() : 0;
+        int nb_spaces_left = nb_spaces / 2;
+        int nb_spaces_right = sColumnWidth - s.size() - nb_spaces_left;
+        if (nb_spaces_right < 0)
+            nb_spaces_right = 0;
+        return std::string((size_t)nb_spaces_left, ' ') + s + std::string( (size_t)nb_spaces_right, ' ');
+    }
+    static std::string align_center(const int v)
+    {
+        return align_center(std::to_string(v));
+    }
+
+    std::string TypeSynonyms::str() const
+    {
+        return    align_center(cv_depth) + align_center(cv_depth_name) 
+                + align_center(np_format) + align_center(np_format_long);
+    }
+
+    
+    std::string _print_types_synonyms_str()
+    {
+        std::string title = 
+              align_center("cv_depth") + align_center("cv_depth_name") 
+            + align_center("np_format") + align_center("np_format_long");; 
+
+        std::string r;
+        r = title + "\n";
+        for (const auto& format: sTypeSynonyms)
+            r = r + format.str() + "\n";
+        return r;
+    }
+
+    std::vector<TypeSynonyms> list_types_synonyms()
+    {
+        return sTypeSynonyms;
+    }
+
+    void print_types_synonyms()
+    {
+        std::cout << _print_types_synonyms_str();
+    }
+}
\ No newline at end of file
diff --git a/cscore/cvnp/cvnp_synonyms.h b/cscore/cvnp/cvnp_synonyms.h
new file mode 100644
index 0000000..09bb9db
--- /dev/null
+++ b/cscore/cvnp/cvnp_synonyms.h
@@ -0,0 +1,25 @@
+#pragma once
+#include <opencv2/core.hpp>
+#include <pybind11/numpy.h>
+#include <string>
+#include <vector>
+
+
+namespace cvnp
+{
+    struct TypeSynonyms
+    {
+        int         cv_depth = -1;
+        std::string cv_depth_name;
+        std::string np_format;
+        std::string np_format_long;
+
+        pybind11::dtype dtype() { return pybind11::dtype(np_format); }
+        std::string str() const;
+    };
+
+    extern std::vector<TypeSynonyms> sTypeSynonyms;
+
+    std::vector<TypeSynonyms> list_types_synonyms();
+    void                      print_types_synonyms();
+}
\ No newline at end of file
diff --git a/cscore/src/main.cpp b/cscore/src/main.cpp
new file mode 100644
index 0000000..8d999cd
--- /dev/null
+++ b/cscore/src/main.cpp
@@ -0,0 +1,19 @@
+
+#include <rpygen_wrapper.hpp>
+
+#include "cscore_cpp.h"
+
+RPYBUILD_PYBIND11_MODULE(m) {
+    initWrapper(m);
+
+    static int unused; // the capsule needs something to reference
+    py::capsule cleanup(&unused, [](void *) {
+        // don't release gil until after calling this
+        cs::SetDefaultLogger(20 /* WPI_LOG_INFO */);
+        
+        // but this MUST release the gil, or deadlock may occur
+        py::gil_scoped_release __release;
+        CS_Shutdown();
+    });
+    m.add_object("_cleanup", cleanup);
+}
\ No newline at end of file
diff --git a/cscore_src b/cscore_src
deleted file mode 160000
index abf56e2..0000000
--- a/cscore_src
+++ /dev/null
@@ -1,1 +0,0 @@
-Subproject commit abf56e2abdf4808f5ee0110943d8305f1aa7de38
diff --git a/devtools/subtree.sh b/devtools/subtree.sh
deleted file mode 100755
index 7e7450c..0000000
--- a/devtools/subtree.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/bin/bash -e
-#
-# Assumes that the allwpilib repo is checked out to the correct branch
-# that needs to be added to this repo
-#
-# This script is only intended to be used by maintainers of this
-# repository, users should not need to use it.
-#
-
-abspath() {
-  # $1 : relative filename
-  echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
-}
-
-if [ "$1" == "" ]; then
-    echo "Usage: $0 /path/to/allwpilib"
-    exit 1
-fi
-
-ALLWPILIB=$(abspath "$1")
-CSCORE_SRC=$(abspath "$(dirname "$0")/../cscore_src")
-
-cd "$ALLWPILIB"
-
-ORIG_BRANCH="$(git symbolic-ref HEAD 2>/dev/null)"
-
-git checkout -b CSCORE_TMP
-
-# TODO: this takes awhile
-git filter-branch -f --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- cscore wpiutil' --prune-empty
-
-cd "$CSCORE_SRC"
-
-git pull "$ALLWPILIB" CSCORE_TMP 
-
-cd "$ALLWPILIB"
-
-git checkout $ORIG_BRANCH
-git branch -D CSCORE_TMP
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..b99dc47
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,5 @@
+/_build
+/_sidebar.rst.inc
+/_sidebar.toml
+/cscore
+cscore.rst
\ No newline at end of file
diff --git a/docs/Makefile b/docs/Makefile
index b8b9c15..d4bb2cb 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,177 +1,20 @@
-# Makefile for Sphinx documentation
+# Minimal makefile for Sphinx documentation
 #
 
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-PAPER         =
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
 BUILDDIR      = _build
 
-# User-friendly check for sphinx-build
-ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
-endif
-
-# Internal variables.
-PAPEROPT_a4     = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
+# Put it first so that "make" without argument is like "make help".
 help:
-	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  html       to make standalone HTML files"
-	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
-	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
-	@echo "  xml        to make Docutils-native XML files"
-	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
-	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
-
-clean:
-	rm -rf $(BUILDDIR)/*
-
-html:
-	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
-	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
-	@echo
-	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
-json:
-	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
-	@echo
-	@echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
-	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
-	@echo
-	@echo "Build finished; now you can run HTML Help Workshop with the" \
-	      ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sphinx.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sphinx.qhc"
-
-devhelp:
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $$HOME/.local/share/devhelp/sphinx"
-	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sphinx"
-	@echo "# devhelp"
-
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through platex and dvipdfmx..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
-	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
-	@echo
-	@echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
-	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
-	@echo
-	@echo "Link check complete; look for any errors in the above output " \
-	      "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
-	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
-	@echo "Testing of doctests in the sources finished, look at the " \
-	      "results in $(BUILDDIR)/doctest/output.txt."
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
-xml:
-	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
-	@echo
-	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+.PHONY: help Makefile
 
-pseudoxml:
-	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
-	@echo
-	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/api.rst b/docs/api.rst
index 0efb777..3dac01c 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -6,6 +6,5 @@ RobotPy CSCore
 
 .. toctree::
     
-    cameraserver
-    objects
+    cscore
     utilities
diff --git a/docs/cameraserver.rst b/docs/cameraserver.rst
deleted file mode 100644
index 60f51ec..0000000
--- a/docs/cameraserver.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-CameraServer
-------------
-
-.. autoclass:: cscore.CameraServer
-    :members:
-    :undoc-members:
-
-.. autoclass:: cscore.VideoException
-    :members:
diff --git a/docs/conf.py b/docs/conf.py
index f3e16f1..7017d0a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -6,53 +6,9 @@
 
 from os.path import abspath, join, dirname
 
-# Insert module path here
-sys.path.insert(0, abspath(dirname(__file__)))
-sys.path.insert(0, abspath(join(dirname(__file__), "..")))
-
-if os.environ.get("GENERATING_CPP") is None:
-    from unittest.mock import Mock
-
-    class FakeType(type):
-        def __getattr__(cls, name):
-            if name.startswith("_"):
-                raise AttributeError(name)
-            return FakeType(
-                name,
-                (),
-                {
-                    "__module__": cls.__module__,
-                    "__qualname__": "%s.%s" % (cls.__name__, name),
-                },
-            )
-
-    class FakeModule:
-        def __init__(self, name):
-            self.__name__ = name
-
-        def __getattr__(self, name):
-            if name.startswith("_"):
-                raise AttributeError(name)
-            if name[0].isupper():
-                return FakeType(name, (), {"__module__": self.__name__})
-            else:
-                return Mock(name=name)
-
-    class Importer:
-        def find_module(self, module_name, package_path):
-            print("%s | %s |" % (module_name, package_path))
-
-        def load_module(self, module_name):
-            print("Load?")
-
-    # print("WT")
-    # sys.meta_path.append(Importer())
-
-    sys.modules["_cscore"] = FakeModule(name="cscore")
-    sys.modules["cscore._init_cscore"] = FakeModule(name="init_cscore")
-    sys.modules["cv2"] = Mock()
-
+# Project must be built+installed to generate docs
 import cscore
+import cscore.version
 
 # -- RTD configuration ------------------------------------------------
 
@@ -71,9 +27,10 @@ def load_module(self, module_name):
 # ones.
 extensions = [
     "sphinx.ext.autodoc",
-    "sphinx.ext.viewcode",
+    "sphinx.ext.autosummary",
     "sphinx.ext.intersphinx",
-    "sphinx_autodoc_typehints",
+    "sphinx.ext.viewcode",
+    "robotpy_sphinx.all",
 ]
 
 # The suffix of source filenames.
@@ -84,11 +41,11 @@ def load_module(self, module_name):
 
 # General information about the project.
 project = "RobotPy CSCore"
-copyright = "2017, RobotPy development team"
+copyright = "2022, RobotPy development team"
 
 intersphinx_mapping = {
-    "networktables": (
-        "http://pynetworktables.readthedocs.io/en/%s/" % rtd_version,
+    "ntcore": (
+        "https://robotpy.readthedocs.io/projects/pyntcore/en/%s/" % rtd_version,
         None,
     )
 }
@@ -98,9 +55,9 @@ def load_module(self, module_name):
 # built documents.
 #
 # The short X.Y version.
-version = ".".join(cscore.__version__.split(".")[:2])
+version = ".".join(cscore.version.version.split(".")[:2])
 # The full version, including alpha/beta/rc tags.
-release = cscore.__version__
+release = cscore.version.version
 
 autoclass_content = "both"
 
@@ -169,6 +126,15 @@ def load_module(self, module_name):
 
 # -- Custom Document processing ----------------------------------------------
 
-import gensidebar
+from robotpy_sphinx.regen import gen_package
+from robotpy_sphinx.sidebar import generate_sidebar
+
+generate_sidebar(
+    globals(),
+    "cscore",
+    "https://raw.githubusercontent.com/robotpy/docs-sidebar/master/sidebar.toml",
+)
+
+root = abspath(dirname(__file__))
 
-gensidebar.generate_sidebar(globals(), "cscore")
+gen_package(root, "cscore")
diff --git a/docs/gencpp.py b/docs/gencpp.py
deleted file mode 100755
index 1e9a0d8..0000000
--- a/docs/gencpp.py
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/env python3
-#
-# The C++ extension cannot be easily built on RTD, so instead periodically
-# the documentation gets automatically generated using this and then it is
-# checked into git.
-#
-# From http://stackoverflow.com/questions/2668187/make-sphinx-generate-rst-class-documentation-from-pydoc
-#
-
-import inspect
-import os
-import re
-from os.path import abspath, exists, dirname, join
-import shutil
-
-import sphinx.cmd.build
-import sphinx.ext.autodoc
-
-root = abspath(dirname(__file__))
-gendir = join(root, "..", "_docgen")
-if exists(gendir):
-    shutil.rmtree(gendir)
-
-os.makedirs(gendir)
-shutil.copy(join(root, "conf.py"), gendir)
-shutil.copy(join(root, "gensidebar.py"), gendir)
-
-os.chdir(gendir)
-
-import _cscore
-
-fns = []
-clss = []
-
-for n in dir(_cscore):
-    if n.startswith("_"):
-        continue
-    o = getattr(_cscore, n)
-    if inspect.isclass(o):
-        clss.append(n)
-    elif inspect.isfunction(o) or inspect.isbuiltin(o):
-        fns.append(n)
-    else:
-        raise ValueError("Unknown thing: %s (%s)" % (n, o))
-
-
-def _print_heading(heading, file, underline="-"):
-    print(heading, file=file)
-    print(underline * len(heading), end="\n\n", file=file)
-
-
-def _writeheader(fp):
-    print(".. THIS FILE IS AUTOGENERATED, DO NOT MODIFY\n", file=fp)
-    _print_heading("Objects", file=fp, underline="=")
-
-
-with open(join(gendir, "index.rst"), "w") as fp:
-    _writeheader(fp)
-
-    for cls in sorted(clss):
-        print(".. autoclass:: cscore.%s" % cls, file=fp)
-        print("   :members:", file=fp)
-        print("   :undoc-members:", file=fp)
-        bases = getattr(_cscore, cls).__bases__
-        if (
-            bases != (object,)
-            and bases[0].__module__ != "pybind11_builtins"
-            and not bases[0].__name__.startswith("_")
-        ):
-            print("   :show-inheritance:", file=fp)
-        fp.write("\n")
-
-    for fn in sorted(fns):
-        print(".. autofunction:: cscore.%s\n" % fn, file=fp)
-
-rst = []
-
-
-def add_line(self, line, source, *lineno):
-    """Append one line of generated reST to the output."""
-    if line:
-        line = self.indent + line
-    rst.append(line)
-    self.directive.result.append(line, source, *lineno)
-
-
-sphinx.ext.autodoc.Documenter.add_line = add_line
-
-os.environ["GENERATING_CPP"] = "1"
-exit_code = sphinx.cmd.build.main(
-    [
-        "-b",
-        "html",
-        "-d",
-        "_build/doctrees",
-        ".",
-        "_build/html",
-        "index.rst",
-    ]
-)
-if exit_code != 0:
-    exit(exit_code)
-
-with open(join(root, "objects.rst"), "w") as fp:
-    _writeheader(fp)
-    no_funcs_yet = True
-
-    # Format the output a bit..
-    for l in rst:
-        l = l.replace("_cscore", "cscore")
-        l = l.replace(
-            "wpi::ArrayRef<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >",
-            "List[str]",
-        )
-        l = re.sub(r"self: \S+\)", ")", l)
-        l = re.sub(r"self: \S+, ", "", l)
-        l = re.sub(r"cs::(\S+)::(\S+)", "cscore.\\1.\\2", l)
-        l = l.replace("cs::", "cscore.")
-        if l.startswith(".. py:class:: "):
-            if l.endswith(" -> None"):
-                l = l[: -len(" -> None")]
-            e = l.find("(")
-            if e == -1:
-                e = len(l)
-            name = l[14:e]
-            if "." not in name:
-                _print_heading(name, fp)
-        elif l.startswith(".. py:function:: ") and no_funcs_yet:
-            no_funcs_yet = False
-            _print_heading("Utility functions", fp)
-
-        print(l, file=fp)
-
-if exists(gendir):
-    shutil.rmtree(gendir)
-print("Done.")
diff --git a/docs/gensidebar.py b/docs/gensidebar.py
deleted file mode 100644
index 28097e5..0000000
--- a/docs/gensidebar.py
+++ /dev/null
@@ -1,97 +0,0 @@
-#
-# This file generates the sidebar/toctree for all RobotPy projects and should
-# be copied to each project when it is updated
-#
-
-import os
-
-
-def write_if_changed(fname, contents):
-
-    try:
-        with open(fname, "r") as fp:
-            old_contents = fp.read()
-    except:
-        old_contents = ""
-
-    if old_contents != contents:
-        with open(fname, "w") as fp:
-            fp.write(contents)
-
-
-def generate_sidebar(conf, conf_api):
-
-    # determine 'latest' or 'stable'
-    # if not conf.do_gen:
-    do_gen = os.environ.get("SIDEBAR", None) == "1" or conf["on_rtd"]
-    version = conf["rtd_version"]
-
-    lines = ["", ".. DO NOT MODIFY! THIS PAGE IS AUTOGENERATED!", ""]
-
-    def toctree(name):
-        lines.extend(
-            [".. toctree::", "    :caption: %s" % name, "    :maxdepth: 2", ""]
-        )
-
-    def endl():
-        lines.append("")
-
-    def write(desc, link):
-        if conf_api == "robotpy":
-            args = desc, link
-        elif not do_gen:
-            return
-        else:
-            args = (
-                desc,
-                "https://robotpy.readthedocs.io/en/%s/%s.html" % (version, link),
-            )
-
-        lines.append("    %s <%s>" % args)
-
-    def write_api(project, desc):
-        if project != conf_api:
-            if do_gen:
-                args = desc, project, version
-                lines.append(
-                    "    %s API <https://robotpy.readthedocs.io/projects/%s/en/%s/api.html>"
-                    % args
-                )
-        else:
-            lines.append("    %s API <api>" % desc)
-
-    #
-    # Specify the sidebar contents here
-    #
-
-    toctree("Robot Programming")
-    write("Getting Started", "getting_started")
-    write("Installation", "install/index")
-    write("Programmer's Guide", "guide/index")
-    write("Frameworks", "frameworks/index")
-    write("Hardware & Sensors", "hw")
-    write("Camera & Vision", "vision/index")
-    endl()
-
-    toctree("API Reference")
-    write_api("wpilib", "WPILib")
-    write_api("pynetworktables", "NetworkTables")
-    write_api("cscore", "CSCore")
-    write_api("utilities", "Utilities")
-    write_api("pyfrc", "PyFRC")
-    write_api("ctre", "CTRE Libraries")
-    write_api("navx", "NavX Library")
-    write_api("rev", "SPARK MAX Library")
-    endl()
-
-    toctree("Additional Info")
-    write("Troubleshooting", "troubleshooting")
-    write("Support", "support")
-    write("FAQ", "faq")
-    endl()
-
-    toctree("RobotPy Developers")
-    write("Developer Documentation", "dev/index")
-    endl()
-
-    write_if_changed("_sidebar.rst.inc", "\n".join(lines))
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index 4bf38ef..0000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,242 +0,0 @@
-@ECHO OFF
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=sphinx-build
-)
-set BUILDDIR=_build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
-set I18NSPHINXOPTS=%SPHINXOPTS% .
-if NOT "%PAPER%" == "" (
-	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
-	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
-)
-
-if "%1" == "" goto help
-
-if "%1" == "help" (
-	:help
-	echo.Please use `make ^<target^>` where ^<target^> is one of
-	echo.  html       to make standalone HTML files
-	echo.  dirhtml    to make HTML files named index.html in directories
-	echo.  singlehtml to make a single large HTML file
-	echo.  pickle     to make pickle files
-	echo.  json       to make JSON files
-	echo.  htmlhelp   to make HTML files and a HTML help project
-	echo.  qthelp     to make HTML files and a qthelp project
-	echo.  devhelp    to make HTML files and a Devhelp project
-	echo.  epub       to make an epub
-	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
-	echo.  text       to make text files
-	echo.  man        to make manual pages
-	echo.  texinfo    to make Texinfo files
-	echo.  gettext    to make PO message catalogs
-	echo.  changes    to make an overview over all changed/added/deprecated items
-	echo.  xml        to make Docutils-native XML files
-	echo.  pseudoxml  to make pseudoxml-XML files for display purposes
-	echo.  linkcheck  to check all external links for integrity
-	echo.  doctest    to run all doctests embedded in the documentation if enabled
-	goto end
-)
-
-if "%1" == "clean" (
-	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
-	del /q /s %BUILDDIR%\*
-	goto end
-)
-
-
-%SPHINXBUILD% 2> nul
-if errorlevel 9009 (
-	echo.
-	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
-	echo.installed, then set the SPHINXBUILD environment variable to point
-	echo.to the full path of the 'sphinx-build' executable. Alternatively you
-	echo.may add the Sphinx directory to PATH.
-	echo.
-	echo.If you don't have Sphinx installed, grab it from
-	echo.http://sphinx-doc.org/
-	exit /b 1
-)
-
-if "%1" == "html" (
-	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
-	goto end
-)
-
-if "%1" == "dirhtml" (
-	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
-	goto end
-)
-
-if "%1" == "singlehtml" (
-	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
-	goto end
-)
-
-if "%1" == "pickle" (
-	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the pickle files.
-	goto end
-)
-
-if "%1" == "json" (
-	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the JSON files.
-	goto end
-)
-
-if "%1" == "htmlhelp" (
-	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
-	goto end
-)
-
-if "%1" == "qthelp" (
-	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
-	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sphinx.qhcp
-	echo.To view the help file:
-	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sphinx.ghc
-	goto end
-)
-
-if "%1" == "devhelp" (
-	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished.
-	goto end
-)
-
-if "%1" == "epub" (
-	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The epub file is in %BUILDDIR%/epub.
-	goto end
-)
-
-if "%1" == "latex" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "latexpdf" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	cd %BUILDDIR%/latex
-	make all-pdf
-	cd %BUILDDIR%/..
-	echo.
-	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "latexpdfja" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	cd %BUILDDIR%/latex
-	make all-pdf-ja
-	cd %BUILDDIR%/..
-	echo.
-	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "text" (
-	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The text files are in %BUILDDIR%/text.
-	goto end
-)
-
-if "%1" == "man" (
-	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The manual pages are in %BUILDDIR%/man.
-	goto end
-)
-
-if "%1" == "texinfo" (
-	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
-	goto end
-)
-
-if "%1" == "gettext" (
-	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
-	goto end
-)
-
-if "%1" == "changes" (
-	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.The overview file is in %BUILDDIR%/changes.
-	goto end
-)
-
-if "%1" == "linkcheck" (
-	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Link check complete; look for any errors in the above output ^
-or in %BUILDDIR%/linkcheck/output.txt.
-	goto end
-)
-
-if "%1" == "doctest" (
-	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
-	goto end
-)
-
-if "%1" == "xml" (
-	%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The XML files are in %BUILDDIR%/xml.
-	goto end
-)
-
-if "%1" == "pseudoxml" (
-	%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
-	goto end
-)
-
-:end
diff --git a/docs/objects.rst b/docs/objects.rst
deleted file mode 100644
index 13cd235..0000000
--- a/docs/objects.rst
+++ /dev/null
@@ -1,1478 +0,0 @@
-.. THIS FILE IS AUTOGENERATED, DO NOT MODIFY
-
-Objects
-=======
-
-
-AxisCamera
-----------
-
-.. py:class:: AxisCamera(*args, **kwargs)
-   :module: cscore
-
-   Bases: :class:`cscore.HttpCamera`
-
-   A source that represents an Axis IP camera.
-
-   Overloaded function.
-
-   1. __init__(name: str, host: str) -> None
-
-   Create a source for a MJPEG-over-HTTP (IP) camera.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param host: Camera host IP or DNS name (e.g. "10.x.x.11")
-   :param kind: Camera kind (e.g. kAxis)
-
-   2. __init__(name: str, hosts: List[str]) -> None
-
-   Create a source for a MJPEG-over-HTTP (IP) camera.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param hosts: Array of Camera host IPs/DNS names
-   :param kind: Camera kind (e.g. kAxis)
-
-
-CvSink
-------
-
-.. py:class:: CvSink(name: str)
-   :module: cscore
-
-   Bases: :class:`cscore.ImageSink`
-
-   A sink for user code to accept video frames as OpenCV images.
-
-   Create a sink for accepting OpenCV images. :meth:`grabFrame` must be called on the created sink to get each new image
-
-   :param name: Source name (arbitrary unique identifier)
-
-
-   .. py:method:: CvSink.grabFrame(image: numpy.ndarray, timeout: float = 0.225) -> Tuple[int, numpy.ndarray]
-      :module: cscore
-
-      Wait for the next frame and get the image. Times out (returning 0) after timeout seconds.
-      The provided image will have three 8-bit channels stored in BGR order.
-
-      :returns: Frame time, or 0 on error (call :meth:`getError` to obtain the error message), returned image
-                The frame time is in 1us increments
-
-
-   .. py:method:: CvSink.grabFrameNoTimeout(image: numpy.ndarray) -> Tuple[int, numpy.ndarray]
-      :module: cscore
-
-      Wait for the next frame and get the image. May block forever.
-      The provided image will have three 8-bit channels stored in BGR order.
-
-      :returns: Frame time, or 0 on error (call :meth:`getError` to obtain the error message), returned image
-                The frame time is in 1us increments
-
-
-   .. py:method:: CvSink.setDescription(description: str) -> None
-      :module: cscore
-
-      Set sink description.
-
-      :param description: Description
-
-
-CvSource
---------
-
-.. py:class:: CvSource(*args, **kwargs)
-   :module: cscore
-
-   Bases: :class:`cscore.ImageSource`
-
-   A source for user code to provide OpenCV images as video frames.
-
-   Overloaded function.
-
-   1. __init__(name: str, mode: cscore.VideoMode) -> None
-
-   Create an OpenCV source.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param mode: Video mode being generated
-
-   2. __init__(name: str, pixelFormat: cscore.VideoMode.PixelFormat, width: int, height: int, fps: int) -> None
-
-   Create an OpenCV source.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param pixelFormat: Pixel format
-   :param width: width
-   :param height: height
-   :param fps: fps
-
-
-   .. py:method:: CvSource.putFrame(image: numpy.ndarray) -> None
-      :module: cscore
-
-      Put an OpenCV image and notify sinks.
-
-      Only 8-bit single-channel or 3-channel (with BGR channel order) images are supported. If the format, depth or channel order is different, use ``cv2.convertTo()`` and/or ``cv2.cvtColor()`` to convert it first.
-
-      :param image: OpenCV image
-
-
-HttpCamera
-----------
-
-.. py:class:: HttpCamera(*args, **kwargs)
-   :module: cscore
-
-   Bases: :class:`cscore.VideoCamera`
-
-   A source that represents a MJPEG-over-HTTP (IP) camera.
-
-   Overloaded function.
-
-   1. __init__(name: str, url: str, kind: cscore.HttpCamera.HttpCameraKind = <HttpCameraKind.kUnknown: 0>) -> None
-
-   Create a source for a MJPEG-over-HTTP (IP) camera.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param url: Camera URL (e.g. "http://10.x.y.11/video/stream.mjpg")
-   :param kind: Camera kind (e.g. kAxis)
-
-   2. __init__(name: str, urls: List[str], kind: cscore.HttpCamera.HttpCameraKind = <HttpCameraKind.kUnknown: 0>) -> None
-
-   Create a source for a MJPEG-over-HTTP (IP) camera.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param urls: Array of Camera URLs
-   :param kind: Camera kind (e.g. kAxis)
-
-
-   .. py:class:: HttpCamera.HttpCameraKind(value: int) -> None
-      :module: cscore
-
-      Bases: :class:`pybind11_builtins.pybind11_object`
-
-      Members:
-
-      kUnknown
-
-      kMJPGStreamer
-
-      kCSCore
-
-      kAxis
-
-
-      .. py:attribute:: HttpCamera.HttpCameraKind.kAxis
-         :module: cscore
-         :value: <HttpCameraKind.kAxis: 3>
-
-
-      .. py:attribute:: HttpCamera.HttpCameraKind.kCSCore
-         :module: cscore
-         :value: <HttpCameraKind.kCSCore: 2>
-
-
-      .. py:attribute:: HttpCamera.HttpCameraKind.kMJPGStreamer
-         :module: cscore
-         :value: <HttpCameraKind.kMJPGStreamer: 1>
-
-
-      .. py:attribute:: HttpCamera.HttpCameraKind.kUnknown
-         :module: cscore
-         :value: <HttpCameraKind.kUnknown: 0>
-
-
-      .. py:method:: HttpCamera.HttpCameraKind.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: HttpCamera.getHttpCameraKind() -> cscore.HttpCamera.HttpCameraKind
-      :module: cscore
-
-      Get the kind of HTTP camera. Autodetection can result in returning a different value than the camera was created with.
-
-
-   .. py:method:: HttpCamera.getUrls() -> List[str]
-      :module: cscore
-
-      Get the URLs used to connect to the camera.
-
-
-   .. py:method:: HttpCamera.setUrls(urls: List[str]) -> None
-      :module: cscore
-
-      Change the URLs used to connect to the camera.
-
-
-ImageSink
----------
-
-.. py:class:: ImageSink
-   :module: cscore
-
-   Bases: :class:`cscore.VideoSink`
-
-   A base class for single image reading sinks.
-
-
-   .. py:method:: ImageSink.getError() -> str
-      :module: cscore
-
-      Get error string.  Call this if :meth:`waitForFrame` returns 0 to determine what the error is.
-
-
-   .. py:method:: ImageSink.setDescription(description: str) -> None
-      :module: cscore
-
-      Set sink description.
-
-      :param description: Description
-
-
-   .. py:method:: ImageSink.setEnabled(enabled: bool) -> None
-      :module: cscore
-
-      Enable or disable getting new frames.
-      Disabling will cause processFrame (for callback-based ImageSinks) to not be called and :meth:`waitForFrame` to not return.  This can be used to save processor resources when frames are not needed.
-
-
-ImageSource
------------
-
-.. py:class:: ImageSource
-   :module: cscore
-
-   Bases: :class:`cscore.VideoSource`
-
-   A base class for single image providing sources.
-
-
-   .. py:method:: ImageSource.createBooleanProperty(name: str, defaultValue: bool, value: bool) -> cscore.VideoProperty
-      :module: cscore
-
-      Create a property.
-
-      :param name: Property name
-      :param defaultValue: Default value
-      :param value: Current value
-
-      :returns: Property
-
-
-   .. py:method:: ImageSource.createIntegerProperty(name: str, minimum: int, maximum: int, step: int, defaultValue: int, value: int) -> cscore.VideoProperty
-      :module: cscore
-
-      Create a property.
-
-      :param name: Property name
-      :param minimum: Minimum value
-      :param maximum: Maximum value
-      :param step: Step value
-      :param defaultValue: Default value
-      :param value: Current value
-
-      :returns: Property
-
-
-   .. py:method:: ImageSource.createProperty(name: str, kind: cscore.VideoProperty.Kind, minimum: int, maximum: int, step: int, defaultValue: int, value: int) -> cscore.VideoProperty
-      :module: cscore
-
-      Create a property.
-
-      :param name: Property name
-      :param kind: Property kind
-      :param minimum: Minimum value
-      :param maximum: Maximum value
-      :param step: Step value
-      :param defaultValue: Default value
-      :param value: Current value
-
-      :returns: Property
-
-
-   .. py:method:: ImageSource.createStringProperty(name: str, value: str) -> cscore.VideoProperty
-      :module: cscore
-
-      Create a property.
-
-      :param name: Property name
-      :param value: Current value
-
-      :returns: Property
-
-
-   .. py:method:: ImageSource.notifyError(msg: str) -> None
-      :module: cscore
-
-      Signal sinks that an error has occurred.  This should be called instead of :meth:`notifyFrame` when an error occurs.
-
-
-   .. py:method:: ImageSource.setConnected(connected: bool) -> None
-      :module: cscore
-
-      Set source connection status.  Defaults to true.
-
-      :param connected: True for connected, false for disconnected
-
-
-   .. py:method:: ImageSource.setDescription(description: str) -> None
-      :module: cscore
-
-      Set source description.
-
-      :param description: Description
-
-
-   .. py:method:: ImageSource.setEnumPropertyChoices(property: cscore.VideoProperty, choices: List[str]) -> None
-      :module: cscore
-
-      Configure enum property choices.
-
-      :param property: Property
-      :param choices: Choices
-
-
-MjpegServer
------------
-
-.. py:class:: MjpegServer(*args, **kwargs)
-   :module: cscore
-
-   Bases: :class:`cscore.VideoSink`
-
-   A sink that acts as a MJPEG-over-HTTP network server.
-
-   Overloaded function.
-
-   1. __init__(name: str, listenAddress: str, port: int) -> None
-
-   Create a MJPEG-over-HTTP server sink.
-
-   :param name: Sink name (arbitrary unique identifier)
-   :param listenAddress: TCP listen address (empty string for all addresses)
-   :param port: TCP port number
-
-   2. __init__(name: str, port: int) -> None
-
-   Create a MJPEG-over-HTTP server sink.
-
-   :param name: Sink name (arbitrary unique identifier)
-   :param port: TCP port number
-
-
-   .. py:method:: MjpegServer.getListenAddress() -> str
-      :module: cscore
-
-      Get the listen address of the server.
-
-
-   .. py:method:: MjpegServer.getPort() -> int
-      :module: cscore
-
-      Get the port number of the server.
-
-
-   .. py:method:: MjpegServer.setCompression(quality: int) -> None
-      :module: cscore
-
-      Set the compression for clients that don't specify it.
-
-      Setting this will result in increased CPU usage for MJPEG source cameras as it will decompress and recompress the image instead of using the camera's MJPEG image directly.
-
-      :param quality: JPEG compression quality (0-100), -1 for unspecified
-
-
-   .. py:method:: MjpegServer.setDefaultCompression(quality: int) -> None
-      :module: cscore
-
-      Set the default compression used for non-MJPEG sources.  If not set, 80 is used.  This function has no effect on MJPEG source cameras; use setCompression() instead to force recompression of MJPEG source images.
-
-      :param quality: JPEG compression quality (0-100)
-
-
-   .. py:method:: MjpegServer.setFPS(fps: int) -> None
-      :module: cscore
-
-      Set the stream frames per second (FPS) for clients that don't specify it.
-
-      It is not necessary to set this if it is the same as the source FPS.
-
-      :param fps: FPS, 0 for unspecified
-
-
-   .. py:method:: MjpegServer.setResolution(width: int, height: int) -> None
-      :module: cscore
-
-      Set the stream resolution for clients that don't specify it.
-
-      It is not necessary to set this if it is the same as the source resolution.
-
-      Setting this different than the source resolution will result in increased CPU usage, particularly for MJPEG source cameras, as it will decompress, resize, and recompress the image, instead of using the camera's MJPEG image directly.
-
-      :param width:  width, 0 for unspecified
-      :param height: height, 0 for unspecified
-
-
-RawEvent
---------
-
-.. py:class:: RawEvent
-   :module: cscore
-
-   Listener event
-
-
-   .. py:class:: RawEvent.Kind(value: int) -> None
-      :module: cscore
-
-      Members:
-
-      kSourceCreated
-
-      kSourceDestroyed
-
-      kSourceConnected
-
-      kSourceDisconnected
-
-      kSourceVideoModesUpdated
-
-      kSourceVideoModeChanged
-
-      kSourcePropertyCreated
-
-      kSourcePropertyValueUpdated
-
-      kSourcePropertyChoicesUpdated
-
-      kSinkSourceChanged
-
-      kSinkCreated
-
-      kSinkDestroyed
-
-      kSinkEnabled
-
-      kSinkDisabled
-
-      kNetworkInterfacesChanged
-
-      kTelemetryUpdated
-
-      kSinkPropertyCreated
-
-      kSinkPropertyValueUpdated
-
-      kSinkPropertyChoicesUpdated
-
-
-      .. py:attribute:: RawEvent.Kind.kNetworkInterfacesChanged
-         :module: cscore
-         :value: <Kind.kNetworkInterfacesChanged: 16384>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkCreated
-         :module: cscore
-         :value: <Kind.kSinkCreated: 1024>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkDestroyed
-         :module: cscore
-         :value: <Kind.kSinkDestroyed: 2048>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkDisabled
-         :module: cscore
-         :value: <Kind.kSinkDisabled: 8192>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkEnabled
-         :module: cscore
-         :value: <Kind.kSinkEnabled: 4096>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkPropertyChoicesUpdated
-         :module: cscore
-         :value: <Kind.kSinkPropertyChoicesUpdated: 262144>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkPropertyCreated
-         :module: cscore
-         :value: <Kind.kSinkPropertyCreated: 65536>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkPropertyValueUpdated
-         :module: cscore
-         :value: <Kind.kSinkPropertyValueUpdated: 131072>
-
-
-      .. py:attribute:: RawEvent.Kind.kSinkSourceChanged
-         :module: cscore
-         :value: <Kind.kSinkSourceChanged: 512>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourceConnected
-         :module: cscore
-         :value: <Kind.kSourceConnected: 4>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourceCreated
-         :module: cscore
-         :value: <Kind.kSourceCreated: 1>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourceDestroyed
-         :module: cscore
-         :value: <Kind.kSourceDestroyed: 2>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourceDisconnected
-         :module: cscore
-         :value: <Kind.kSourceDisconnected: 8>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourcePropertyChoicesUpdated
-         :module: cscore
-         :value: <Kind.kSourcePropertyChoicesUpdated: 256>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourcePropertyCreated
-         :module: cscore
-         :value: <Kind.kSourcePropertyCreated: 64>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourcePropertyValueUpdated
-         :module: cscore
-         :value: <Kind.kSourcePropertyValueUpdated: 128>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourceVideoModeChanged
-         :module: cscore
-         :value: <Kind.kSourceVideoModeChanged: 32>
-
-
-      .. py:attribute:: RawEvent.Kind.kSourceVideoModesUpdated
-         :module: cscore
-         :value: <Kind.kSourceVideoModesUpdated: 16>
-
-
-      .. py:attribute:: RawEvent.Kind.kTelemetryUpdated
-         :module: cscore
-         :value: <Kind.kTelemetryUpdated: 32768>
-
-
-      .. py:method:: RawEvent.Kind.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: RawEvent.kind
-      :module: cscore
-      :property:
-
-
-   .. py:method:: RawEvent.mode
-      :module: cscore
-      :property:
-
-
-   .. py:method:: RawEvent.name
-      :module: cscore
-      :property:
-
-
-   .. py:method:: RawEvent.sinkHandle
-      :module: cscore
-      :property:
-
-
-   .. py:method:: RawEvent.sourceHandle
-      :module: cscore
-      :property:
-
-
-   .. py:method:: RawEvent.value
-      :module: cscore
-      :property:
-
-
-   .. py:method:: RawEvent.valueStr
-      :module: cscore
-      :property:
-
-
-UsbCamera
----------
-
-.. py:class:: UsbCamera(*args, **kwargs)
-   :module: cscore
-
-   Bases: :class:`cscore.VideoCamera`
-
-   A source that represents a USB camera.
-
-   Overloaded function.
-
-   1. __init__(name: str, dev: int) -> None
-
-   Create a source for a USB camera based on device number.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param dev: Device number (e.g. 0 for ``/dev/video0``)
-
-   2. __init__(name: str, path: str) -> None
-
-   Create a source for a USB camera based on device path.
-
-   :param name: Source name (arbitrary unique identifier)
-   :param path: Path to device (e.g. ``/dev/video0`` on Linux)
-
-
-   .. py:method:: UsbCamera.enumerateUsbCameras() -> List[cscore.UsbCameraInfo]
-      :module: cscore
-      :staticmethod:
-
-      Enumerate USB cameras on the local system.
-
-      :returns: list of USB camera information (one for each camera)
-
-
-   .. py:method:: UsbCamera.getInfo() -> cscore.UsbCameraInfo
-      :module: cscore
-
-      Get the full camera information for the device.
-
-
-   .. py:method:: UsbCamera.getPath() -> str
-      :module: cscore
-
-      Get the path to the device.
-
-
-   .. py:method:: UsbCamera.setConnectVerbose(level: int) -> None
-      :module: cscore
-
-      Set how verbose the camera connection messages are.
-
-      :param level: 0=don't display Connecting message, 1=do display message
-
-
-   .. py:method:: UsbCamera.setPath(path: str) -> None
-      :module: cscore
-
-      Change the path to the device.
-
-
-UsbCameraInfo
--------------
-
-.. py:class:: UsbCameraInfo
-   :module: cscore
-
-   USB camera information
-
-
-   .. py:method:: UsbCameraInfo.dev
-      :module: cscore
-      :property:
-
-
-   .. py:method:: UsbCameraInfo.name
-      :module: cscore
-      :property:
-
-
-   .. py:method:: UsbCameraInfo.otherPaths
-      :module: cscore
-      :property:
-
-
-   .. py:method:: UsbCameraInfo.path
-      :module: cscore
-      :property:
-
-
-   .. py:method:: UsbCameraInfo.productId
-      :module: cscore
-      :property:
-
-
-   .. py:method:: UsbCameraInfo.vendorId
-      :module: cscore
-      :property:
-
-
-VideoCamera
------------
-
-.. py:class:: VideoCamera
-   :module: cscore
-
-   Bases: :class:`cscore.VideoSource`
-
-   A source that represents a video camera.
-
-
-   .. py:class:: VideoCamera.WhiteBalance(value: int) -> None
-      :module: cscore
-
-      Bases: :class:`pybind11_builtins.pybind11_object`
-
-      Members:
-
-      kFixedIndoor
-
-      kFixedOutdoor1
-
-      kFixedOutdoor2
-
-      kFixedFluorescent1
-
-      kFixedFlourescent2
-
-
-      .. py:attribute:: VideoCamera.WhiteBalance.kFixedFlourescent2
-         :module: cscore
-         :value: <WhiteBalance.kFixedFlourescent2: 5200>
-
-
-      .. py:attribute:: VideoCamera.WhiteBalance.kFixedFluorescent1
-         :module: cscore
-         :value: <WhiteBalance.kFixedFluorescent1: 5100>
-
-
-      .. py:attribute:: VideoCamera.WhiteBalance.kFixedIndoor
-         :module: cscore
-         :value: <WhiteBalance.kFixedIndoor: 3000>
-
-
-      .. py:attribute:: VideoCamera.WhiteBalance.kFixedOutdoor1
-         :module: cscore
-         :value: <WhiteBalance.kFixedOutdoor1: 4000>
-
-
-      .. py:attribute:: VideoCamera.WhiteBalance.kFixedOutdoor2
-         :module: cscore
-         :value: <WhiteBalance.kFixedOutdoor2: 5000>
-
-
-      .. py:method:: VideoCamera.WhiteBalance.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: VideoCamera.getBrightness() -> int
-      :module: cscore
-
-      Get the brightness, as a percentage (0-100).
-
-
-   .. py:method:: VideoCamera.setBrightness(brightness: int) -> None
-      :module: cscore
-
-      Set the brightness, as a percentage (0-100).
-
-
-   .. py:method:: VideoCamera.setExposureAuto() -> None
-      :module: cscore
-
-      Set the exposure to auto aperature.
-
-
-   .. py:method:: VideoCamera.setExposureHoldCurrent() -> None
-      :module: cscore
-
-      Set the exposure to hold current.
-
-
-   .. py:method:: VideoCamera.setExposureManual(value: int) -> None
-      :module: cscore
-
-      Set the exposure to manual, as a percentage (0-100).
-
-
-   .. py:method:: VideoCamera.setWhiteBalanceAuto() -> None
-      :module: cscore
-
-      Set the white balance to auto.
-
-
-   .. py:method:: VideoCamera.setWhiteBalanceHoldCurrent() -> None
-      :module: cscore
-
-      Set the white balance to hold current.
-
-
-   .. py:method:: VideoCamera.setWhiteBalanceManual(value: int) -> None
-      :module: cscore
-
-      Set the white balance to manual, with specified color temperature.
-
-
-VideoEvent
-----------
-
-.. py:class:: VideoEvent
-   :module: cscore
-
-   Bases: :class:`cscore.RawEvent`
-
-   An event generated by the library and provided to event listeners.
-
-
-   .. py:method:: VideoEvent.getProperty() -> cscore.VideoProperty
-      :module: cscore
-
-
-   .. py:method:: VideoEvent.getSink() -> cscore.VideoSink
-      :module: cscore
-
-
-   .. py:method:: VideoEvent.getSource() -> cscore.VideoSource
-      :module: cscore
-
-
-VideoListener
--------------
-
-.. py:class:: VideoListener(callback: Callable[[cscore.VideoEvent], None], eventMask: int, immediateNotify: bool)
-   :module: cscore
-
-   An event listener.  This calls back to a desigated callback function when
-   an event matching the specified mask is generated by the library.
-
-   Create an event listener.
-
-   :param callback: Callback function
-   :param eventMask: Bitmask of VideoEvent.Kind values
-   :param immediateNotify: Whether callback should be immediately called with a representative set of events for the current library state.
-
-
-VideoMode
----------
-
-.. py:class:: VideoMode(pixelFormat: cscore.VideoMode.PixelFormat, width: int, height: int, fps: int)
-   :module: cscore
-
-   Video mode
-
-
-   .. py:class:: VideoMode.PixelFormat(value: int) -> None
-      :module: cscore
-
-      Members:
-
-      kUnknown
-
-      kMJPEG
-
-      kYUYV
-
-      kRGB565
-
-      kBGR
-
-      kGray
-
-
-      .. py:attribute:: VideoMode.PixelFormat.kBGR
-         :module: cscore
-         :value: <PixelFormat.kBGR: 4>
-
-
-      .. py:attribute:: VideoMode.PixelFormat.kGray
-         :module: cscore
-         :value: <PixelFormat.kGray: 5>
-
-
-      .. py:attribute:: VideoMode.PixelFormat.kMJPEG
-         :module: cscore
-         :value: <PixelFormat.kMJPEG: 1>
-
-
-      .. py:attribute:: VideoMode.PixelFormat.kRGB565
-         :module: cscore
-         :value: <PixelFormat.kRGB565: 3>
-
-
-      .. py:attribute:: VideoMode.PixelFormat.kUnknown
-         :module: cscore
-         :value: <PixelFormat.kUnknown: 0>
-
-
-      .. py:attribute:: VideoMode.PixelFormat.kYUYV
-         :module: cscore
-         :value: <PixelFormat.kYUYV: 2>
-
-
-      .. py:method:: VideoMode.PixelFormat.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: VideoMode.fps
-      :module: cscore
-      :property:
-
-
-   .. py:method:: VideoMode.height
-      :module: cscore
-      :property:
-
-
-   .. py:method:: VideoMode.pixelFormat
-      :module: cscore
-      :property:
-
-
-   .. py:method:: VideoMode.width
-      :module: cscore
-      :property:
-
-
-VideoProperty
--------------
-
-.. py:class:: VideoProperty
-   :module: cscore
-
-   A source or sink property.
-
-
-   .. py:class:: VideoProperty.Kind(value: int) -> None
-      :module: cscore
-
-      Members:
-
-      kNone
-
-      kBoolean
-
-      kInteger
-
-      kString
-
-      kEnum
-
-
-      .. py:attribute:: VideoProperty.Kind.kBoolean
-         :module: cscore
-         :value: <Kind.kBoolean: 1>
-
-
-      .. py:attribute:: VideoProperty.Kind.kEnum
-         :module: cscore
-         :value: <Kind.kEnum: 8>
-
-
-      .. py:attribute:: VideoProperty.Kind.kInteger
-         :module: cscore
-         :value: <Kind.kInteger: 2>
-
-
-      .. py:attribute:: VideoProperty.Kind.kNone
-         :module: cscore
-         :value: <Kind.kNone: 0>
-
-
-      .. py:attribute:: VideoProperty.Kind.kString
-         :module: cscore
-         :value: <Kind.kString: 4>
-
-
-      .. py:method:: VideoProperty.Kind.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: VideoProperty.get() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getChoices() -> List[str]
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getDefault() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getKind() -> cscore.VideoProperty.Kind
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getLastStatus() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getMax() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getMin() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getName() -> str
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getStep() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.getString() -> str
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.isBoolean() -> bool
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.isEnum() -> bool
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.isInteger() -> bool
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.isString() -> bool
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.set(value: int) -> None
-      :module: cscore
-
-
-   .. py:method:: VideoProperty.setString(value: str) -> None
-      :module: cscore
-
-
-VideoSink
----------
-
-.. py:class:: VideoSink(sink: cscore.VideoSink)
-   :module: cscore
-
-   A sink for video that accepts a sequence of frames.
-
-
-   .. py:class:: VideoSink.Kind(value: int) -> None
-      :module: cscore
-
-      Members:
-
-      kUnknown
-
-      kMjpeg
-
-      kCv
-
-
-      .. py:attribute:: VideoSink.Kind.kCv
-         :module: cscore
-         :value: <Kind.kCv: 4>
-
-
-      .. py:attribute:: VideoSink.Kind.kMjpeg
-         :module: cscore
-         :value: <Kind.kMjpeg: 2>
-
-
-      .. py:attribute:: VideoSink.Kind.kUnknown
-         :module: cscore
-         :value: <Kind.kUnknown: 0>
-
-
-      .. py:method:: VideoSink.Kind.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: VideoSink.enumerateProperties() -> List[cscore.VideoProperty]
-      :module: cscore
-
-      Enumerate all properties of this sink
-
-
-   .. py:method:: VideoSink.enumerateSinks() -> List[cscore.VideoSink]
-      :module: cscore
-      :staticmethod:
-
-      Enumerate all existing sinks.
-
-      :returns: list of sinks.
-
-
-   .. py:method:: VideoSink.getConfigJson() -> str
-      :module: cscore
-
-      Get a JSON configuration string.
-
-      :returns: JSON configuration string
-
-
-   .. py:method:: VideoSink.getDescription() -> str
-      :module: cscore
-
-      Get the sink description.  This is sink-kind specific.
-
-
-   .. py:method:: VideoSink.getHandle() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoSink.getKind() -> cscore.VideoSink.Kind
-      :module: cscore
-
-      Get the kind of the sink.
-
-
-   .. py:method:: VideoSink.getLastStatus() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoSink.getName() -> str
-      :module: cscore
-
-      Get the name of the sink.  The name is an arbitrary identifier provided when the sink is created, and should be unique.
-
-
-   .. py:method:: VideoSink.getProperty(name: str) -> cscore.VideoProperty
-      :module: cscore
-
-      Get a property.
-
-      :param name: Property name
-      :returns: Property contents (VideoSource.Kind.kNone if no property with the given name exists)
-
-
-   .. py:method:: VideoSink.getSource() -> cscore.VideoSource
-      :module: cscore
-
-      Get the connected source.
-
-      :returns: Connected source (empty if none connected).
-
-
-   .. py:method:: VideoSink.getSourceProperty(name: str) -> cscore.VideoProperty
-      :module: cscore
-
-      Get a property of the associated source.
-
-      :param name: Property name
-      :returns: Property (VideoSink.Kind.kNone if no property with the given name exists or no source connected)
-
-
-   .. py:method:: VideoSink.setConfigJson(config: str) -> bool
-      :module: cscore
-
-      Set properties from a JSON configuration string.
-
-      The format of the JSON input is::
-
-          {
-            "properties": [
-              {
-                "name": "property name",
-                "value": "property value"
-               }
-            ]
-          }
-
-      :param config: configuration
-      :returns: True if set successfully
-
-
-   .. py:method:: VideoSink.setSource(source: cscore.VideoSource) -> None
-      :module: cscore
-
-      Configure which source should provide frames to this sink.  Each sink can accept frames from only a single source, but a single source can provide frames to multiple clients.
-
-      :param source: Source
-
-
-VideoSource
------------
-
-.. py:class:: VideoSource(source: cscore.VideoSource)
-   :module: cscore
-
-   A source for video that provides a sequence of frames.
-
-
-   .. py:class:: VideoSource.ConnectionStrategy(value: int) -> None
-      :module: cscore
-
-      Members:
-
-      kAutoManage
-
-      kKeepOpen
-
-      kForceClose
-
-
-      .. py:attribute:: VideoSource.ConnectionStrategy.kAutoManage
-         :module: cscore
-         :value: <ConnectionStrategy.kAutoManage: 0>
-
-
-      .. py:attribute:: VideoSource.ConnectionStrategy.kForceClose
-         :module: cscore
-         :value: <ConnectionStrategy.kForceClose: 2>
-
-
-      .. py:attribute:: VideoSource.ConnectionStrategy.kKeepOpen
-         :module: cscore
-         :value: <ConnectionStrategy.kKeepOpen: 1>
-
-
-      .. py:method:: VideoSource.ConnectionStrategy.name
-         :module: cscore
-         :property:
-
-
-   .. py:class:: VideoSource.Kind(value: int) -> None
-      :module: cscore
-
-      Members:
-
-      kUnknown
-
-      kUsb
-
-      kHttp
-
-      kCv
-
-
-      .. py:attribute:: VideoSource.Kind.kCv
-         :module: cscore
-         :value: <Kind.kCv: 4>
-
-
-      .. py:attribute:: VideoSource.Kind.kHttp
-         :module: cscore
-         :value: <Kind.kHttp: 2>
-
-
-      .. py:attribute:: VideoSource.Kind.kUnknown
-         :module: cscore
-         :value: <Kind.kUnknown: 0>
-
-
-      .. py:attribute:: VideoSource.Kind.kUsb
-         :module: cscore
-         :value: <Kind.kUsb: 1>
-
-
-      .. py:method:: VideoSource.Kind.name
-         :module: cscore
-         :property:
-
-
-   .. py:method:: VideoSource.enumerateProperties() -> List[cscore.VideoProperty]
-      :module: cscore
-
-      Enumerate all properties of this source
-
-
-   .. py:method:: VideoSource.enumerateSinks() -> List[cscore.VideoSink]
-      :module: cscore
-
-      Enumerate all sinks connected to this source.
-
-      :returns: list of sinks.
-
-
-   .. py:method:: VideoSource.enumerateSources() -> List[cscore.VideoSource]
-      :module: cscore
-      :staticmethod:
-
-      Enumerate all existing sources.
-
-      :returns: list of sources.
-
-
-   .. py:method:: VideoSource.enumerateVideoModes() -> List[cscore.VideoMode]
-      :module: cscore
-
-      Enumerate all known video modes for this source.
-
-
-   .. py:method:: VideoSource.getActualDataRate() -> float
-      :module: cscore
-
-      Get the data rate (in bytes per second).
-
-      :func:`.setTelemetryPeriod` must be called for this to be valid.
-
-      :returns: Data rate averaged over the telemetry period.
-
-
-   .. py:method:: VideoSource.getActualFPS() -> float
-      :module: cscore
-
-      Get the actual FPS.
-
-      :func:`.setTelemetryPeriod` must be called for this to be valid.
-
-      :returns: Actual FPS averaged over the telemetry period.
-
-
-   .. py:method:: VideoSource.getConfigJson() -> str
-      :module: cscore
-
-      Get a JSON configuration string.
-
-      :returns: JSON string
-
-
-   .. py:method:: VideoSource.getDescription() -> str
-      :module: cscore
-
-      Get the source description.  This is source-kind specific.
-
-
-   .. py:method:: VideoSource.getHandle() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoSource.getKind() -> cscore.VideoSource.Kind
-      :module: cscore
-
-      Get the kind of the source
-
-
-   .. py:method:: VideoSource.getLastFrameTime() -> int
-      :module: cscore
-
-      Get the last time a frame was captured.
-
-
-   .. py:method:: VideoSource.getLastStatus() -> int
-      :module: cscore
-
-
-   .. py:method:: VideoSource.getName() -> str
-      :module: cscore
-
-      Get the name of the source. The name is an arbitrary identifier provided when the source is created, and should be unique.
-
-
-   .. py:method:: VideoSource.getProperty(name: str) -> cscore.VideoProperty
-      :module: cscore
-
-      Get a property.
-
-      :param name: Property name
-      :returns: Property contents (VideoSource.Kind.kNone if no property with the given name exists)
-
-
-   .. py:method:: VideoSource.getVideoMode() -> cscore.VideoMode
-      :module: cscore
-
-      Get the current video mode.
-
-
-   .. py:method:: VideoSource.isConnected() -> bool
-      :module: cscore
-
-      Is the source currently connected to whatever is providing the images?
-
-
-   .. py:method:: VideoSource.setConfigJson(config: str) -> bool
-      :module: cscore
-
-      Set video mode and properties from a JSON configuration string.
-
-      :param config: Configuration
-      :returns: True if set successfully
-
-
-   .. py:method:: VideoSource.setConnectionStrategy(strategy: cscore.VideoSource.ConnectionStrategy) -> None
-      :module: cscore
-
-      Set the connection strategy.  By default, the source will automatically connect or disconnect based on whether any sinks are connected.
-
-      :param strategy: connection strategy (see ConnectionStrategy)
-
-
-   .. py:method:: VideoSource.setFPS(fps: int) -> bool
-      :module: cscore
-
-      Set the frames per second (FPS).
-
-      :param fps: desired FPS
-      :returns: True if set successfully
-
-
-   .. py:method:: VideoSource.setPixelFormat(pixelFormat: cscore.VideoMode.PixelFormat) -> bool
-      :module: cscore
-
-      Set the pixel format.
-
-      :param pixelFormat: desired pixel format
-      :returns: True if set successfully
-
-
-   .. py:method:: VideoSource.setResolution(width: int, height: int) -> bool
-      :module: cscore
-
-      Set the resolution.
-
-      :param width: desired width
-      :param height: desired height
-      :returns: True if set successfully
-
-
-   .. py:method:: VideoSource.setVideoMode(*args, **kwargs)
-      :module: cscore
-
-      Overloaded function.
-
-      1. setVideoMode(mode: cscore.VideoMode) -> bool
-
-      Set the video mode.
-
-      :param mode: Video mode
-
-      2. setVideoMode(pixelFormat: cscore.VideoMode.PixelFormat, width: int, height: int, fps: int) -> bool
-
-      Set the video mode.
-
-      :param pixelFormat: desired pixel format
-      :param width: desired width
-      :param height: desired height
-      :param fps: desired FPS
-      :returns: True if set successfully
-
-
-Utility functions
------------------
-
-.. py:function:: getHttpCameraUrls(source: int) -> List[str]
-   :module: cscore
-
-
-.. py:function:: getNetworkInterfaces() -> List[str]
-   :module: cscore
-
-
-.. py:function:: getTelemetryElapsedTime() -> float
-   :module: cscore
-
-
-.. py:function:: getUsbCameraPath(source: int) -> str
-   :module: cscore
-
-
-.. py:function:: setLogger(func: Callable[[int, str, int, str], None], min_level: int) -> None
-   :module: cscore
-
-
-.. py:function:: setTelemetryPeriod(seconds: float) -> None
-   :module: cscore
-
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 309003d..1aee532 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,5 @@
-sphinx-autodoc-typehints
+# This file is intended for use on readthedocs
+sphinx >= 3.0
 sphinx-rtd-theme
-numpy
-pynetworktables
+robotpy-sphinx-plugin
+-e docs
\ No newline at end of file
diff --git a/docs/setup.py b/docs/setup.py
new file mode 100644
index 0000000..1740242
--- /dev/null
+++ b/docs/setup.py
@@ -0,0 +1,23 @@
+# this is a dirty hack to convince readthedocs to install a specific
+# version of our software. We assume that this will always be triggered
+# on a specific version
+
+from setuptools import setup
+
+try:
+    from setuptools_scm import get_version
+except ImportError:
+    import subprocess
+
+    def get_version(*args, **kwargs):
+        return subprocess.check_output(["git", "describe", "--tags"], encoding="utf-8")
+
+
+package = "robotpy-cscore"
+version = get_version(root="..", relative_to=__file__)
+
+setup(
+    name="dummy-package",
+    version="1.0",
+    install_requires=["%s==%s" % (package, version)],
+)
diff --git a/examples/cvstream.py b/examples/cvstream.py
index c4f64b9..67dbe3c 100755
--- a/examples/cvstream.py
+++ b/examples/cvstream.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 #
 # WARNING: You should only use this approach for testing cscore on platforms that
-#          it doesn't support using UsbCamera (Windows or OSX).
+#          it doesn't support using UsbCamera
 #
 
 import cscore as cs
diff --git a/examples/dual_cameraserver.py b/examples/dual_cameraserver.py
index feaa9cc..a123628 100755
--- a/examples/dual_cameraserver.py
+++ b/examples/dual_cameraserver.py
@@ -8,17 +8,16 @@
 # in the same program as your robot code!
 #
 
-from cscore import CameraServer, UsbCamera
+from cscore import CameraServer as CS
 
 
 def main():
-    cs = CameraServer.getInstance()
-    cs.enableLogging()
+    CS.enableLogging()
 
-    usb1 = cs.startAutomaticCapture(dev=0)
-    usb2 = cs.startAutomaticCapture(dev=1)
+    usb1 = CS.startAutomaticCapture(dev=0)
+    usb2 = CS.startAutomaticCapture(dev=1)
 
-    cs.waitForever()
+    CS.waitForever()
 
 
 if __name__ == "__main__":
@@ -29,7 +28,9 @@ def main():
     logging.basicConfig(level=logging.DEBUG)
 
     # You should uncomment these to connect to the RoboRIO
-    # import networktables
-    # networktables.initialize(server='10.xx.xx.2')
+    # import ntcore
+    # nt = ntcore.NetworkTableInstance.getDefault()
+    # nt.setServerTeam(XXXX)
+    # nt.startClient4(__file__)
 
     main()
diff --git a/examples/intermediate_cameraserver.py b/examples/intermediate_cameraserver.py
index 3061358..b469156 100755
--- a/examples/intermediate_cameraserver.py
+++ b/examples/intermediate_cameraserver.py
@@ -12,22 +12,21 @@
 import cv2
 import numpy as np
 
-from cscore import CameraServer
+from cscore import CameraServer as CS
 
 
 def main():
-    cs = CameraServer.getInstance()
-    cs.enableLogging()
+    CS.enableLogging()
 
-    camera = cs.startAutomaticCapture()
+    camera = CS.startAutomaticCapture()
 
     camera.setResolution(640, 480)
 
     # Get a CvSink. This will capture images from the camera
-    cvSink = cs.getVideo()
+    cvSink = CS.getVideo()
 
     # (optional) Setup a CvSource. This will send images back to the Dashboard
-    outputStream = cs.putVideo("Rectangle", 640, 480)
+    outputStream = CS.putVideo("Rectangle", 640, 480)
 
     # Allocating new images is very expensive, always try to preallocate
     img = np.zeros(shape=(480, 640, 3), dtype=np.uint8)
@@ -57,7 +56,9 @@ def main():
     logging.basicConfig(level=logging.DEBUG)
 
     # You should uncomment these to connect to the RoboRIO
-    # import networktables
-    # networktables.initialize(server='10.xx.xx.2')
+    # import ntcore
+    # nt = ntcore.NetworkTableInstance.getDefault()
+    # nt.setServerTeam(XXXX)
+    # nt.startClient4(__file__)
 
     main()
diff --git a/examples/quick_cameraserver.py b/examples/quick_cameraserver.py
index 719591e..24ccdad 100755
--- a/examples/quick_cameraserver.py
+++ b/examples/quick_cameraserver.py
@@ -8,15 +8,14 @@
 #
 
 
-from cscore import CameraServer
+from cscore import CameraServer as CS
 
 
 def main():
-    cs = CameraServer.getInstance()
-    cs.enableLogging()
+    CS.enableLogging()
 
-    cs.startAutomaticCapture()
-    cs.waitForever()
+    CS.startAutomaticCapture()
+    CS.waitForever()
 
 
 if __name__ == "__main__":
@@ -27,7 +26,9 @@ def main():
     logging.basicConfig(level=logging.DEBUG)
 
     # You should uncomment these to connect to the RoboRIO
-    # import networktables
-    # networktables.initialize(server='10.xx.xx.2')
+    # import ntcore
+    # nt = ntcore.NetworkTableInstance.getDefault()
+    # nt.setServerTeam(XXXX)
+    # nt.startClient4(__file__)
 
     main()
diff --git a/examples/switched_cameraserver.py b/examples/switched_cameraserver.py
index d518018..123d53a 100755
--- a/examples/switched_cameraserver.py
+++ b/examples/switched_cameraserver.py
@@ -8,18 +8,18 @@
 # in the same program as your robot code!
 #
 
-from cscore import CameraServer, UsbCamera
-import networktables
+from cscore import CameraServer as CS
+from cscore import UsbCamera
+import ntcore
 
 
 def main():
-    cs = CameraServer.getInstance()
-    cs.enableLogging()
+    CS.enableLogging()
 
     usb0 = UsbCamera("Camera 0", 0)
     usb1 = UsbCamera("Camera 1", 1)
 
-    server = cs.addSwitchedCamera("Switched")
+    server = CS.addSwitchedCamera("Switched")
     server.setSource(usb0)
 
     # Use networktables to switch the source
@@ -30,11 +30,11 @@ def _listener(source, key, value, isNew):
         elif str(value) == "1":
             server.setSource(usb1)
 
-    table = networktables.NetworkTables.getTable("/CameraPublisher")
+    table = ntcore.NetworkTableInstance.getDefault().getTable("/CameraPublisher")
     table.putString("selected", "0")
     table.addEntryListener(_listener, key="selected")
 
-    cs.waitForever()
+    CS.waitForever()
 
 
 if __name__ == "__main__":
@@ -45,6 +45,8 @@ def _listener(source, key, value, isNew):
     logging.basicConfig(level=logging.DEBUG)
 
     # You should change this to connect to the RoboRIO
-    networktables.NetworkTables.initialize(server="localhost")
+    nt = ntcore.NetworkTableInstance.getDefault()
+    nt.setServer("localhost")
+    nt.startClient4(__file__)
 
     main()
diff --git a/gen/CameraServer.yml b/gen/CameraServer.yml
new file mode 100644
index 0000000..db3dd4a
--- /dev/null
+++ b/gen/CameraServer.yml
@@ -0,0 +1,69 @@
+---
+
+extra_includes:
+- optional
+
+classes:
+  CameraServer:
+    attributes:
+      kBasePort:
+      kSize640x480:
+      kSize320x240:
+      kSize160x120:
+    methods:
+      StartAutomaticCapture:
+        overloads:
+          "":
+          int:
+          std::string_view, int:
+          std::string_view, std::string_view:
+          cs::VideoSource&:
+      AddAxisCamera:
+        overloads:
+          std::string_view:
+          char*:
+            ignore: true
+          std::string&:
+            ignore: true
+          std::span<const std::string>:
+          std::initializer_list<T>:
+            ignore: true
+          std::string_view, std::string_view:
+          std::string_view, char*:
+            ignore: true
+          std::string_view, std::string&:
+            ignore: true
+          std::string_view, std::span<const std::string>:
+          std::string_view, std::initializer_list<T>:
+            ignore: true
+      AddSwitchedCamera:
+      GetVideo:
+        overloads:
+          "":
+          cs::VideoSource&:
+          std::string_view:
+      PutVideo:
+      AddServer:
+        overloads:
+          std::string_view:
+          std::string_view, int:
+          cs::VideoSink&:
+      RemoveServer:
+      GetServer:
+        overloads:
+          "":
+          std::string_view:
+      AddCamera:
+      RemoveCamera:
+      SetSize:
+    inline_code: |
+      .def_static("waitForever", []() {
+        auto time_module = py::module::import("time");
+        while (true) {
+          time_module.attr("sleep")(1);
+        }
+      }, py::doc("Infinitely loops until the process dies"))
+      .def_static("enableLogging", [](std::optional<int> level) {
+        auto lm = py::module::import("cscore._logging");
+        lm.attr("enableLogging")(level);
+      }, py::arg("level") = py::none(), py::doc("Enable cscore logging"))
\ No newline at end of file
diff --git a/gen/cscore_cpp.yml b/gen/cscore_cpp.yml
new file mode 100644
index 0000000..588cf31
--- /dev/null
+++ b/gen/cscore_cpp.yml
@@ -0,0 +1,306 @@
+---
+
+functions:
+  GetPropertyKind:
+    ignore: true
+  GetPropertyName:
+    overloads:
+      CS_Property, CS_Status*:
+        ignore: true
+      CS_Property, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  GetProperty:
+    ignore: true
+  SetProperty:
+    ignore: true
+  GetPropertyMin:
+    ignore: true
+  GetPropertyMax:
+    ignore: true
+  GetPropertyStep:
+    ignore: true
+  GetPropertyDefault:
+    ignore: true
+  GetStringProperty:
+    overloads:
+      CS_Property, CS_Status*:
+        ignore: true
+      CS_Property, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  SetStringProperty:
+    ignore: true
+  GetEnumPropertyChoices:
+    ignore: true
+  CreateUsbCameraDev:
+    ignore: true
+  CreateUsbCameraPath:
+    ignore: true
+  CreateHttpCamera:
+    overloads:
+      std::string_view, std::string_view, CS_HttpCameraKind, CS_Status*:
+        ignore: true
+      std::string_view, std::span<const std::string>, CS_HttpCameraKind, CS_Status*:
+        ignore: true
+  CreateCvSource:
+    ignore: true
+  GetSourceKind:
+    ignore: true
+  GetSourceName:
+    overloads:
+      CS_Source, CS_Status*:
+        ignore: true
+      CS_Source, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  GetSourceDescription:
+    overloads:
+      CS_Source, CS_Status*:
+        ignore: true
+      CS_Source, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  GetSourceLastFrameTime:
+    ignore: true
+  SetSourceConnectionStrategy:
+    ignore: true
+  IsSourceConnected:
+    ignore: true
+  IsSourceEnabled:
+    ignore: true
+  GetSourceProperty:
+    ignore: true
+  EnumerateSourceProperties:
+    ignore: true
+  GetSourceVideoMode:
+    ignore: true
+  SetSourceVideoMode:
+    ignore: true
+  SetSourcePixelFormat:
+    ignore: true
+  SetSourceResolution:
+    ignore: true
+  SetSourceFPS:
+    ignore: true
+  SetSourceConfigJson:
+    overloads:
+      CS_Source, std::string_view, CS_Status*:
+        ignore: true
+      CS_Source, wpi::json&, CS_Status*:
+        ignore: true
+  GetSourceConfigJson:
+    ignore: true
+  GetSourceConfigJsonObject:
+    ignore: true
+  EnumerateSourceVideoModes:
+    ignore: true
+  EnumerateSourceSinks:
+    ignore: true
+  CopySource:
+    ignore: true
+  ReleaseSource:
+    ignore: true
+  SetCameraBrightness:
+    ignore: true
+  GetCameraBrightness:
+    ignore: true
+  SetCameraWhiteBalanceAuto:
+    ignore: true
+  SetCameraWhiteBalanceHoldCurrent:
+    ignore: true
+  SetCameraWhiteBalanceManual:
+    ignore: true
+  SetCameraExposureAuto:
+    ignore: true
+  SetCameraExposureHoldCurrent:
+    ignore: true
+  SetCameraExposureManual:
+    ignore: true
+  SetUsbCameraPath:
+    ignore: true
+  GetUsbCameraPath:
+    ignore: true
+  GetUsbCameraInfo:
+    ignore: true
+  GetHttpCameraKind:
+    ignore: true
+  SetHttpCameraUrls:
+    ignore: true
+  GetHttpCameraUrls:
+    ignore: true
+  NotifySourceError:
+    ignore: true
+  SetSourceConnected:
+    ignore: true
+  SetSourceDescription:
+    ignore: true
+  CreateSourceProperty:
+    ignore: true
+  SetSourceEnumPropertyChoices:
+    ignore: true
+  CreateMjpegServer:
+    ignore: true
+  CreateCvSink:
+    ignore: true
+  CreateCvSinkCallback:
+    ignore: true
+  GetSinkKind:
+    ignore: true
+  GetSinkName:
+    overloads:
+      CS_Sink, CS_Status*:
+        ignore: true
+      CS_Sink, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  GetSinkDescription:
+    overloads:
+      CS_Sink, CS_Status*:
+        ignore: true
+      CS_Sink, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  GetSinkProperty:
+    ignore: true
+  EnumerateSinkProperties:
+    ignore: true
+  SetSinkSource:
+    ignore: true
+  GetSinkSourceProperty:
+    ignore: true
+  SetSinkConfigJson:
+    overloads:
+      CS_Sink, std::string_view, CS_Status*:
+        ignore: true
+      CS_Sink, wpi::json&, CS_Status*:
+        ignore: true
+  GetSinkConfigJson:
+    ignore: true
+  GetSinkConfigJsonObject:
+    ignore: true
+  GetSinkSource:
+    ignore: true
+  CopySink:
+    ignore: true
+  ReleaseSink:
+    ignore: true
+  GetMjpegServerListenAddress:
+    ignore: true
+  GetMjpegServerPort:
+    ignore: true
+  SetSinkDescription:
+    ignore: true
+  GetSinkError:
+    overloads:
+      CS_Sink, CS_Status*:
+        ignore: true
+      CS_Sink, wpi::SmallVectorImpl<char>&, CS_Status*:
+        ignore: true
+  SetSinkEnabled:
+    ignore: true
+  SetListenerOnStart:
+    ignore: true
+  SetListenerOnExit:
+    ignore: true
+  AddListener:
+    ignore: true
+  RemoveListener:
+    ignore: true
+  CreateListenerPoller:
+    ignore: true
+  DestroyListenerPoller:
+    ignore: true
+  AddPolledListener:
+    ignore: true
+  PollListener:
+    overloads:
+      CS_ListenerPoller:
+        ignore: true
+      CS_ListenerPoller, double, bool*:
+        ignore: true
+  CancelPollListener:
+    ignore: true
+  NotifierDestroyed:
+    ignore: true
+  SetTelemetryPeriod:
+    ignore: true
+  GetTelemetryElapsedTime:
+    ignore: true
+  GetTelemetryValue:
+    ignore: true
+  GetTelemetryAverageValue:
+    ignore: true
+  SetLogger:
+    internal: true
+  SetDefaultLogger:
+    ignore: true
+  Shutdown:
+    ignore: true
+  EnumerateUsbCameras:
+    ignore: true
+  EnumerateSourceHandles:
+    ignore: true
+  EnumerateSinkHandles:
+    ignore: true
+  GetHostname:
+    ignore: true
+  GetNetworkInterfaces:
+    ignore: true
+classes:
+  UsbCameraInfo:
+    attributes:
+      dev:
+      path:
+      name:
+      otherPaths:
+      vendorId:
+      productId:
+  VideoMode:
+    force_no_trampoline: true
+    ignored_bases:
+    - CS_VideoMode
+    enums:
+      PixelFormat:
+    methods:
+      VideoMode:
+        overloads:
+          "":
+          cs::VideoMode::PixelFormat, int, int, int:
+      operator==:
+      operator!=:
+    inline_code: |
+      .def_readwrite("pixelFormat", &VideoMode::pixelFormat)
+      .def_readwrite("width", &VideoMode::width)
+      .def_readwrite("height", &VideoMode::height)
+      .def_readwrite("fps", &VideoMode::fps)
+  RawEvent:
+    attributes:
+      kind:
+        ignore: true
+      sourceHandle:
+        ignore: true
+      sinkHandle:
+        ignore: true
+      name:
+        ignore: true
+      mode:
+        ignore: true
+      propertyHandle:
+        ignore: true
+      propertyKind:
+        ignore: true
+      value:
+        ignore: true
+      valueStr:
+        ignore: true
+    enums:
+      Kind:
+        ignore: true
+    methods:
+      RawEvent:
+        overloads:
+          "":
+            ignore: true
+          cs::RawEvent::Kind:
+            ignore: true
+          std::string_view, CS_Handle, cs::RawEvent::Kind:
+            ignore: true
+          std::string_view, CS_Source, cs::VideoMode&:
+            ignore: true
+          std::string_view, CS_Source, cs::RawEvent::Kind, CS_Property, CS_PropertyKind, int, std::string_view:
+            ignore: true
diff --git a/gen/cscore_cv.yml b/gen/cscore_cv.yml
new file mode 100644
index 0000000..2094eec
--- /dev/null
+++ b/gen/cscore_cv.yml
@@ -0,0 +1,63 @@
+---
+
+extra_includes:
+- opencv2/core/core.hpp
+- cvnp/cvnp.h
+
+functions:
+  CS_PutSourceFrame:
+    ignore: true
+  CS_GrabSinkFrame:
+    ignore: true
+  CS_GrabSinkFrameTimeout:
+    ignore: true
+  CS_GrabSinkFrameCpp:
+    ignore: true
+  CS_GrabSinkFrameTimeoutCpp:
+    ignore: true
+  CS_PutSourceFrameCpp:
+    ignore: true
+  PutSourceFrame:
+    ignore: true
+  GrabSinkFrame:
+    ignore: true
+  GrabSinkFrameTimeout:
+    ignore: true
+classes:
+  CvSource:
+    doc: A source for user code to provide OpenCV images as video frames.
+    force_no_trampoline: true
+    methods:
+      CvSource:
+        overloads:
+          "":
+            ignore: true
+          std::string_view, VideoMode&:
+          std::string_view, VideoMode::PixelFormat, int, int, int:
+      PutFrame:
+  CvSink:
+    doc: A sink for user code to accept video frames as OpenCV images.
+    force_no_trampoline: true
+    methods:
+      CvSink:
+        overloads:
+          "":
+            ignore: true
+          std::string_view:
+          std::string_view, std::function<void ( uint64_t time )>:
+            ignore: true
+      GrabFrame:
+        cpp_code: |
+          [](cs::CvSink &self, cv::Mat& image, double timeout) -> std::tuple<uint64_t, cv::Mat> {
+            py::gil_scoped_release unlock;
+            auto result = self.GrabFrame(image, timeout);
+            return std::make_tuple(result, image);
+          }
+      GrabFrameNoTimeout:
+        cpp_code: |
+          [](cs::CvSink &self, cv::Mat& image) -> std::tuple<uint64_t, cv::Mat> {
+            py::gil_scoped_release unlock;
+            auto result = self.GrabFrameNoTimeout(image);
+            return std::make_tuple(result, image);
+          }
+
diff --git a/gen/cscore_oo.yml b/gen/cscore_oo.yml
new file mode 100644
index 0000000..fc5da09
--- /dev/null
+++ b/gen/cscore_oo.yml
@@ -0,0 +1,215 @@
+---
+
+classes:
+  VideoProperty:
+    enums:
+      Kind:
+    methods:
+      VideoProperty:
+        overloads:
+          "":
+      GetName:
+      GetKind:
+      IsBoolean:
+      IsInteger:
+      IsString:
+      IsEnum:
+      Get:
+      Set:
+      GetMin:
+      GetMax:
+      GetStep:
+      GetDefault:
+      GetString:
+        overloads:
+          '[const]':
+          wpi::SmallVectorImpl<char>& [const]:
+      SetString:
+      GetChoices:
+      GetLastStatus:
+  VideoSource:
+    enums:
+      Kind:
+      ConnectionStrategy:
+    methods:
+      VideoSource:
+        overloads:
+          "":
+          CS_Source:
+      GetHandle:
+      operator==:
+      operator!=:
+      GetKind:
+      GetName:
+      GetDescription:
+      GetLastFrameTime:
+      SetConnectionStrategy:
+      IsConnected:
+      IsEnabled:
+      GetProperty:
+      EnumerateProperties:
+      GetVideoMode:
+      SetVideoMode:
+        overloads:
+          VideoMode&:
+          VideoMode::PixelFormat, int, int, int:
+      SetPixelFormat:
+      SetResolution:
+      SetFPS:
+      SetConfigJson:
+        overloads:
+          std::string_view:
+          wpi::json&:
+      GetConfigJson:
+      GetConfigJsonObject:
+      GetActualFPS:
+      GetActualDataRate:
+      EnumerateVideoModes:
+      GetLastStatus:
+      EnumerateSinks:
+      EnumerateSources:
+      swap:
+        ignore: true
+  VideoCamera:
+    force_no_trampoline: true
+    enums:
+      WhiteBalance:
+    methods:
+      VideoCamera:
+        overloads:
+          "":
+          CS_Source:
+      SetBrightness:
+      GetBrightness:
+      SetWhiteBalanceAuto:
+      SetWhiteBalanceHoldCurrent:
+      SetWhiteBalanceManual:
+      SetExposureAuto:
+      SetExposureHoldCurrent:
+      SetExposureManual:
+  UsbCamera:
+    force_no_trampoline: true
+    methods:
+      UsbCamera:
+        overloads:
+          "":
+          std::string_view, int:
+          std::string_view, std::string_view:
+      EnumerateUsbCameras:
+      SetPath:
+      GetPath:
+      GetInfo:
+      SetConnectVerbose:
+  HttpCamera:
+    force_no_trampoline: true
+    constants:
+    - cs::HttpCamera::HttpCameraKind::kUnknown
+    enums:
+      HttpCameraKind:
+    methods:
+      HttpCamera:
+        overloads:
+          std::string_view, std::string_view, cs::HttpCamera::HttpCameraKind:
+          std::string_view, char*, cs::HttpCamera::HttpCameraKind:
+          std::string_view, std::string&, cs::HttpCamera::HttpCameraKind:
+          std::string_view, std::span<const std::string>, cs::HttpCamera::HttpCameraKind:
+          std::string_view, std::initializer_list<T>, cs::HttpCamera::HttpCameraKind:
+            ignore: true
+      GetHttpCameraKind:
+      SetUrls:
+        overloads:
+          std::span<const std::string>:
+          std::initializer_list<T>:
+            ignore: true
+      GetUrls:
+  AxisCamera:
+    force_no_trampoline: true
+    methods:
+      AxisCamera:
+        overloads:
+          std::string_view, std::string_view:
+          std::string_view, char*:
+          std::string_view, std::string&:
+          std::string_view, std::span<const std::string>:
+          std::string_view, std::initializer_list<T>:
+            ignore: true
+  ImageSource:
+    force_no_trampoline: true
+    methods:
+      NotifyError:
+      SetConnected:
+      SetDescription:
+      CreateProperty:
+      CreateIntegerProperty:
+      CreateBooleanProperty:
+      CreateStringProperty:
+      SetEnumPropertyChoices:
+        overloads:
+          cs::VideoProperty&, std::span<const std::string>:
+          cs::VideoProperty&, std::initializer_list<T>:
+            ignore: true
+      ImageSource:
+  VideoSink:
+    enums:
+      Kind:
+    methods:
+      VideoSink:
+        overloads:
+          "":
+          CS_Sink:
+      GetHandle:
+      operator==:
+      operator!=:
+      GetKind:
+      GetName:
+      GetDescription:
+      GetProperty:
+      EnumerateProperties:
+      SetConfigJson:
+        overloads:
+          std::string_view:
+          wpi::json&:
+      GetConfigJson:
+      GetConfigJsonObject:
+      SetSource:
+      GetSource:
+      GetSourceProperty:
+      GetLastStatus:
+      EnumerateSinks:
+      swap:
+        ignore: true
+  MjpegServer:
+    force_no_trampoline: true
+    methods:
+      MjpegServer:
+        overloads:
+          "":
+          std::string_view, std::string_view, int:
+          std::string_view, int:
+      GetListenAddress:
+      GetPort:
+      SetResolution:
+      SetFPS:
+      SetCompression:
+      SetDefaultCompression:
+  ImageSink:
+    force_no_trampoline: true
+    methods:
+      SetDescription:
+      GetError:
+      SetEnabled:
+      ImageSink:
+  VideoEvent:
+    force_no_trampoline: true
+    methods:
+      GetSource:
+      GetSink:
+      GetProperty:
+  VideoListener:
+    methods:
+      VideoListener:
+        overloads:
+          "":
+          std::function<void ( const VideoEvent & event )>, int, bool:
+      swap:
+        ignore: true
diff --git a/pybind11 b/pybind11
deleted file mode 160000
index 67b55bc..0000000
--- a/pybind11
+++ /dev/null
@@ -1,1 +0,0 @@
-Subproject commit 67b55bcf7887aad358a6bc91c73e07d1920e96bd
diff --git a/pyproject.toml b/pyproject.toml
index 1b2f9e1..f10cce3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,20 +1,78 @@
+[tool.robotpy-build.metadata]
+name = "robotpy-cscore"
+description = "RobotPy bindings for cscore image processing library"
+author = "RobotPy Development Team"
+author_email = "robotpy@googlegroups.com"
+url = "https://github.com/robotpy/robotpy-cscore"
+license = "BSD-3-Clause"
+install_requires = [
+    "robotpy-wpiutil<2024.0.0,>=2023.0.0b4",
+    "robotpy-wpinet<2024.0.0,>=2023.0.0b4",
+    "pyntcore<2024.0.0,>=2023.0.0b4",
+    
+    # basic CameraServer works without NumPy/OpenCV installed, so only require
+    # them on a RoboRIO -- for other platforms there are a lot of ways to
+    # install OpenCV and we shouldn't dictate it
+    'numpy; platform_machine == "armv7l"',
+    'robotpy-opencv; platform_machine == "armv7l"'
+]
+
 [build-system]
-requires = ["distro; platform_system == 'Linux'"]
-
-[tool.black]
-exclude = '''
-/(
-    \.eggs
-  | \.git
-  | \.hg
-  | \.mypy_cache
-  | \.tox
-  | \.venv
-  | _build
-  | buck-out
-  | build
-  | dist
-  | pybind11
-  | cscore_src
-)/
-'''
+requires = [
+    "robotpy-build<2024.0.0,>=2023.0.0b15",
+    "robotpy-wpiutil<2024.0.0,>=2023.0.0b4",
+    "robotpy-wpinet<2024.0.0,>=2023.0.0b4",
+    "pyntcore<2024.0.0,>=2023.0.0b4",
+]
+
+[tool.robotpy-build]
+base_package = "cscore"
+
+[tool.robotpy-build.static_libs."cscore_cpp".maven_lib_download]
+artifact_id = "cscore-cpp"
+group_id = "edu.wpi.first.cscore"
+repo_url = "https://frcmaven.wpi.edu/artifactory/release"
+version = "2023.1.1-beta-4"
+
+libs = ["cscore"]
+
+[tool.robotpy-build.static_libs."cameraserver_cpp".maven_lib_download]
+artifact_id = "cameraserver-cpp"
+group_id = "edu.wpi.first.cameraserver"
+repo_url = "https://frcmaven.wpi.edu/artifactory/release"
+version = "2023.1.1-beta-4"
+
+libs = ["cameraserver"]
+
+[tool.robotpy-build.static_libs."opencv_cpp".maven_lib_download]
+artifact_id = "opencv-cpp"
+group_id = "edu.wpi.first.thirdparty.frc2023.opencv"
+repo_url = "https://frcmaven.wpi.edu/artifactory/release"
+version = "4.6.0-4"
+
+libs = ["opencv460"]
+
+[tool.robotpy-build.wrappers."cscore"]
+name = "cscore"
+
+sources = [
+  "cscore/src/main.cpp",
+  "cscore/cvnp/cvnp.cpp",
+  "cscore/cvnp/cvnp_synonyms.cpp",
+]
+
+depends = ["wpiutil", "wpinet", "ntcore", "opencv_cpp", "cscore_cpp", "cameraserver_cpp", ]
+generation_data = "gen"
+
+[tool.robotpy-build.wrappers."cscore".autogen_headers]
+# cameraserver
+CameraServer = "cameraserver/CameraServer.h"
+
+# cscore
+cscore_cpp = "cscore_cpp.h"
+cscore_oo = "cscore_oo.h"
+cscore_cv = "cscore_cv.h"
+
+[[tool.robotpy-build.wrappers."cscore".type_casters]]
+header = "cvnp/cvnp.h"
+types = ["cv::Mat"]
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 73d2720..3542d0c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,450 +1,5 @@
-#
-# Much of this copied from https://github.com/pybind/python_example.git
-#
+#!/usr/bin/env python3
 
-import sys
+from robotpy_build.setup import setup
 
-if sys.version_info[0] < 3:
-    sys.stderr.write("ERROR: robotpy-cscore requires python 3!")
-    exit(1)
-
-import fnmatch
-import glob
-import os
-from os.path import dirname, exists, join
-from setuptools import find_packages, setup, Extension
-from setuptools.command.build_ext import build_ext
-import subprocess
-import setuptools
-import importlib.util
-import re
-
-setup_dir = dirname(__file__)
-git_dir = join(setup_dir, ".git")
-base_package = "cscore"
-version_file = join(setup_dir, base_package, "version.py")
-
-# Automatically generate a version.py based on the git version
-if exists(git_dir):
-    p = subprocess.Popen(
-        ["git", "describe", "--tags", "--long", "--dirty=-dirty"],
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-    )
-    out, err = p.communicate()
-    # Make sure the git version has at least one tag
-    if err:
-        print("Error: You need to create a tag for this repo to use the builder")
-        sys.exit(1)
-
-    # Convert git version to PEP440 compliant version
-    # - Older versions of pip choke on local identifiers, so we can't include the git commit
-    v, commits, local = out.decode("utf-8").rstrip().split("-", 2)
-    if commits != "0" or "-dirty" in local:
-        v = "%s.post0.dev%s" % (v, commits)
-
-    # Create the version.py file
-    with open(version_file, "w") as fp:
-        fp.write("# Autogenerated by setup.py\n__version__ = '{0}'".format(v))
-
-if exists(version_file):
-    with open(join(setup_dir, base_package, "version.py"), "r") as fp:
-        exec(fp.read(), globals())
-else:
-    __version__ = "master"
-
-with open(join(setup_dir, "README.rst"), "r") as readme_file:
-    long_description = readme_file.read()
-
-
-# detect crossenv
-cross_compiling = getattr(sys, "cross_compiling", False)
-
-install_requires = ["numpy", "pynetworktables"]
-try:
-    import distro
-
-    if distro.id() == "nilrt":
-        install_requires.append('robotpy-opencv; platform_machine == "armv7l"')
-except:
-    pass
-
-
-def detect_external_wpiutil():
-    with open(join(setup_dir, base_package, "_init_cscore.py"), "w") as fp:
-        fp.write("# THIS FILE IS AUTOMATICALLY GENERATED\n")
-        spec = importlib.util.find_spec("wpiutil")
-        wpiutil_path = None
-        if spec:
-            wpiutil_path = dirname(spec.origin)
-            print("Using external wpiutil at", wpiutil_path)
-
-            fp.write("import wpiutil")
-            install_requires.append("robotpy-wpiutil")
-        else:
-            print("Not using external wpiutil")
-        return wpiutil_path
-
-
-# detect robotpy-wpiutil
-external_wpiutil = detect_external_wpiutil()
-
-
-# parallel hack
-parallel = int(os.environ.get("RPYBUILD_PARALLEL", "0"))
-if parallel > 0:
-    # don't enable this hack by default, because not really sure of the
-    # ramifications -- however, it's really useful for development
-    #
-    # .. the real answer to this is cmake o_O
-
-    # monkey-patch for parallel compilation
-    # -> https://stackoverflow.com/questions/11013851/speeding-up-build-process-with-distutils/13176803#13176803
-    def parallelCCompile(
-        self,
-        sources,
-        output_dir=None,
-        macros=None,
-        include_dirs=None,
-        debug=0,
-        extra_preargs=None,
-        extra_postargs=None,
-        depends=None,
-    ):
-        # those lines are copied from distutils.ccompiler.CCompiler directly
-        macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
-            output_dir, macros, include_dirs, sources, depends, extra_postargs
-        )
-        cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
-        # parallel code
-        import multiprocessing
-        import multiprocessing.pool
-
-        N = multiprocessing.cpu_count() if parallel == 1 else parallel
-
-        def _single_compile(obj):
-            try:
-                src, ext = build[obj]
-            except KeyError:
-                return
-            self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
-
-        for _ in multiprocessing.pool.ThreadPool(N).imap(_single_compile, objects):
-            pass
-        return objects
-
-    import distutils.ccompiler
-
-    distutils.ccompiler.CCompiler.compile = parallelCCompile
-
-
-# try to use pkgconfig to find compile parameters for OpenCV
-# Note: pkg-config is available for Windows, so try it on all platforms
-# default: no additional directories needed
-opencv_pkg = {"include_dirs": [], "library_dirs": []}
-if cross_compiling:
-    import sysconfig
-
-    sysconf_data = sysconfig.get_paths()["data"]
-    opencv_pkg = {
-        "include_dirs": [join(sysconf_data, "include", "opencv4")],
-        "library_dirs": [join(sysconf_data, "lib")],
-    }
-else:
-    try:
-        import pkgconfig
-    except ModuleNotFoundError:
-        print(
-            "WARNING: could not import pkgconfig, build will fail with opencv4",
-            file=sys.stderr,
-        )
-    else:
-        try:
-            if pkgconfig.exists("opencv4"):
-                opencv_pkg = pkgconfig.parse("opencv4")
-            elif pkgconfig.exists("opencv"):
-                opencv_pkg = pkgconfig.parse("opencv")
-            else:
-                print(
-                    "WARNING: pkg-config could not find OpenCV. Continuing anyway.",
-                    file=sys.stderr,
-                )
-                print(
-                    "  If you compiled OpenCV, build with -DOPENCV_GENERATE_PKGCONFIG=ON",
-                    file=sys.stderr,
-                )
-        except EnvironmentError as ex:
-            # failed to run pkg-config
-            print("WARNING:", ex, "- continuing anyway", file=sys.stderr)
-
-
-#
-# pybind-specific compilation stuff
-#
-
-
-class get_numpy_include(object):
-
-    if cross_compiling:
-
-        def __str__(self):
-            spec = importlib.util.find_spec("numpy")
-            numpy_root = dirname(spec.origin)
-            return join(numpy_root, "core", "include")
-
-    else:
-
-        def __str__(self):
-            import numpy as np
-
-            return np.get_include()
-
-
-class get_pybind11_include:
-    def __str__(self):
-        import pybind11
-
-        print("Using installed (out-of-tree) pybind11.")
-        return pybind11.get_include()
-
-
-pybind11_include = "pybind11/include"
-if not exists(pybind11_include):
-    pybind11_include = get_pybind11_include()
-
-
-def get_opencv_lib(name):
-    lib = "opencv_" + name
-    if sys.platform == "win32":
-        import cv2
-
-        lib += cv2.__version__.replace(".", "")
-    return lib
-
-
-# cf http://bugs.python.org/issue26689
-def has_flag(compiler, flag):
-    """Return a boolean indicating whether a flag name is supported on
-    the specified compiler.
-    """
-    import tempfile
-
-    with tempfile.TemporaryDirectory() as tmpdir:
-        fname = join(tmpdir, "flagcheck.cpp")
-        with open(fname, "w") as f:
-            # Don't trigger -Wunused-parameter.
-            f.write("int main (int, char **) { return 0; }")
-
-        try:
-            compiler.compile([fname], output_dir=tmpdir, extra_postargs=[flag])
-        except setuptools.distutils.errors.CompileError:
-            return False
-    return True
-
-
-def cpp_flag(compiler, pfx, sep="="):
-    """Return the -std=c++[17/20] compiler flag.
-    The newer version is prefered when it is available).
-    """
-
-    flags = [
-        f"{pfx}std{sep}c++20",
-        f"{pfx}std{sep}c++17",
-    ]
-
-    for flag in flags:
-        if has_flag(compiler, flag):
-            return flag
-
-    raise RuntimeError("Unsupported compiler -- at least C++17 support is needed!")
-
-
-class BuildExt(build_ext):
-    """A custom build extension for adding compiler-specific options."""
-
-    c_opts = {"msvc": ["/EHsc", "/DNOMINMAX"], "unix": []}
-
-    if sys.platform == "darwin":
-        c_opts["unix"] += ["-stdlib=libc++", "-mmacosx-version-min=10.14"]
-
-    def build_extensions(self):
-        ct = self.compiler.compiler_type
-        opts = self.c_opts.get(ct, [])
-        if ct == "unix":
-            # TODO: this feels like a hack
-            if not os.environ.get("RPY_DEBUG"):
-                opts.append("-s")  # strip
-                opts.append("-g0")  # remove debug symbols
-            else:
-                opts.append("-O0")
-            opts.append(cpp_flag(self.compiler, "-"))
-            if has_flag(self.compiler, "-fvisibility=hidden"):
-                opts.append("-fvisibility=hidden")
-            if sys.platform != "darwin":
-                opts.append("-D_GNU_SOURCE")
-        elif ct == "msvc":
-            opts.append(cpp_flag(self.compiler, "/", ":"))
-            opts.append("/Zc:__cplusplus")
-        for ext in self.extensions:
-            ext.define_macros.append(("PYBIND11_USE_SMART_HOLDER_AS_DEFAULT", "1"))
-            ext.extra_compile_args = opts
-        build_ext.build_extensions(self)
-
-
-def recursive_glob(d):
-    for root, _, filenames in os.walk(d):
-        for fname in fnmatch.filter(filenames, "*.cpp"):
-            yield join(root, fname)
-
-
-def get_cscore_sources(d):
-    l = list(glob.glob(d + "/cpp/*.cpp"))
-    if sys.platform == "win32":
-        l.extend(glob.glob(d + "/windows/*.cpp"))
-    elif sys.platform == "darwin":
-        l.extend(glob.glob(d + "/osx/*.cpp"))
-    else:
-        l.extend(glob.glob(d + "/linux/*.cpp"))
-    return l
-
-
-def get_wpiutil_sources(d):
-    if external_wpiutil:
-        return []
-
-    jni = re.compile(r"[\\/]jni[\\/]")
-    l = [f for f in recursive_glob(d + "/cpp") if not jni.search(f)]
-    if sys.platform == "win32":
-        l.extend(glob.glob(d + "/windows/*.cpp"))
-    else:
-        l.extend(glob.glob(d + "/unix/*.cpp"))
-        if sys.platform == "darwin":
-            l.extend(glob.glob(d + "/macOS/*.cpp"))
-        else:
-            l.extend(glob.glob(d + "/linux/*.cpp"))
-    return l
-
-
-def get_wpiutil_includes():
-    if external_wpiutil:
-        return [join(external_wpiutil, "include")]
-
-    return [
-        "cscore_src/wpiutil/src/main/native/fmtlib/include",
-        "cscore_src/wpiutil/src/main/native/include",
-        "cscore_src/wpiutil/src/main/native/libuv/src",
-        "cscore_src/wpiutil/src/main/native/libuv/include",
-    ]
-
-
-def get_wpiutil_libs():
-    if external_wpiutil:
-        return ["wpiutil"]
-    return []
-
-
-def get_wpiutil_libdirs():
-    if external_wpiutil:
-        return [join(external_wpiutil, "lib")]
-    return []
-
-
-def get_libuv_sources(d):
-    if external_wpiutil:
-        return []
-
-    l = list(glob.glob(d + "/*.cpp"))
-    if sys.platform == "win32":
-        l.extend(glob.glob(d + "/win/*.cpp"))
-    else:
-        l.extend(
-            d + "/unix/" + f
-            for f in [
-                "async.cpp",
-                "core.cpp",
-                "dl.cpp",
-                "fs.cpp",
-                "getaddrinfo.cpp",
-                "getnameinfo.cpp",
-                "loop-watcher.cpp",
-                "loop.cpp",
-                "pipe.cpp",
-                "poll.cpp",
-                "process.cpp",
-                "signal.cpp",
-                "stream.cpp",
-                "tcp.cpp",
-                "thread.cpp",
-                "tty.cpp",
-                "udp.cpp",
-            ]
-        )
-        if sys.platform == "darwin":
-            l.extend(
-                d + "/unix/" + f
-                for f in [
-                    "bsd-ifaddrs.cpp",
-                    "darwin.cpp",
-                    "darwin-proctitle.cpp",
-                    "fsevents.cpp",
-                    "kqueue.cpp",
-                    "proctitle.cpp",
-                ]
-            )
-        else:
-            l.extend(
-                d + "/unix/" + f
-                for f in [
-                    "linux-core.cpp",
-                    "linux-inotify.cpp",
-                    "linux-syscalls.cpp",
-                    "procfs-exepath.cpp",
-                    "proctitle.cpp",
-                    "sysinfo-loadavg.cpp",
-                    # "sysinfo-memory.cpp",
-                ]
-            )
-    return l
-
-
-ext_modules = [
-    Extension(
-        "_cscore",
-        ["src/_cscore.cpp", "src/ndarray_converter.cpp"]
-        + get_cscore_sources("cscore_src/cscore/src/main/native")
-        + get_wpiutil_sources("cscore_src/wpiutil/src/main/native")
-        + ["cscore_src/wpiutil/src/main/native/fmtlib/src/format.cpp"]
-        + get_libuv_sources("cscore_src/wpiutil/src/main/native/libuv/src"),
-        include_dirs=[
-            pybind11_include,
-            "cscore_src/cscore/src/main/native/include",
-            "cscore_src/cscore/src/main/native/cpp",
-            get_numpy_include(),
-        ]
-        + get_wpiutil_includes()
-        + opencv_pkg["include_dirs"],
-        library_dirs=opencv_pkg["library_dirs"] + get_wpiutil_libdirs(),
-        libraries=[
-            get_opencv_lib(name) for name in ("core", "highgui", "imgproc", "imgcodecs")
-        ]
-        + get_wpiutil_libs(),
-        language="c++",
-    )
-]
-
-setup(
-    name="robotpy-cscore",
-    version=__version__,
-    author="Dustin Spicuzza",
-    author_email="dustin@virtualroadside.com",
-    url="https://github.com/robotpy/robotpy-cscore",
-    description="RobotPy bindings for cscore image processing library",
-    long_description=long_description,
-    packages=find_packages(),
-    ext_modules=ext_modules,
-    install_requires=install_requires,
-    license="BSD",
-    zip_safe=False,
-    cmdclass={"build_ext": BuildExt},
-    entry_points={"robotpylib": ["info = cscore._info:Info"]},
-)
+setup()
diff --git a/src/_cscore.cpp b/src/_cscore.cpp
deleted file mode 100644
index 9e96e7e..0000000
--- a/src/_cscore.cpp
+++ /dev/null
@@ -1,656 +0,0 @@
-
-#include <pybind11/pybind11.h>
-#include <pybind11/functional.h>
-#include <pybind11/operators.h>
-#include <pybind11/stl.h>
-namespace py = pybind11;
-
-#include "cscore_oo.h"
-#include "cscore_cv.h"
-using namespace cs;
-
-#include <opencv2/opencv.hpp>
-
-#include "ndarray_converter.h"
-#include "wpiutil_converters.hpp"
-
-
-// Use this to release the gil
-// -> Prefer to not use this for short inline getters
-typedef py::call_guard<py::gil_scoped_release> release_gil;
-
-
-
-// Wrapper for VideoListener to prevent crashes on shutdown
-class VideoListenerWrapper : public VideoListener {
-public:
-    VideoListenerWrapper(std::function<void(const VideoEvent&)> callback, int eventMask, bool immediateNotify) :
-        VideoListener(callback, eventMask, immediateNotify),
-        m_callback(callback)
-    {}
-
-    virtual ~VideoListenerWrapper() {}
-
-private:
-    // need a reference to this callback otherwise cscore crashes on exit
-    std::function<void(const VideoEvent&)> m_callback;
-};
-
-
-PYBIND11_MODULE(_cscore, m) {
-
-    NDArrayConverter::init_numpy();
-
-    static int unused; // the capsule needs something to reference
-    py::capsule cleanup(&unused, [](void *) {
-        // don't release gil until after calling this
-        SetDefaultLogger(20 /* WPI_LOG_INFO */);
-        
-        // but this MUST release the gil, or deadlock may occur
-        py::gil_scoped_release __release;
-        CS_Shutdown();
-    });
-    m.add_object("_cleanup", cleanup);
-    
-    // cscore_cpp.h
-    
-    py::class_<UsbCameraInfo> usbcamerainfo(m, "UsbCameraInfo");
-    usbcamerainfo.doc() = "USB camera information";
-    usbcamerainfo
-      .def_readwrite("dev", &UsbCameraInfo::dev)
-      .def_readwrite("path", &UsbCameraInfo::path)
-      .def_readwrite("name", &UsbCameraInfo::name)
-      .def_readwrite("otherPaths", &UsbCameraInfo::otherPaths)
-      .def_readwrite("vendorId", &UsbCameraInfo::vendorId)
-      .def_readwrite("productId", &UsbCameraInfo::productId)
-      .def("__repr__", [](UsbCameraInfo &__inst){
-         return std::string("<UsbCameraInfo dev=") + std::to_string(__inst.dev) + " path=" + __inst.path + " name=" + __inst.name + ">";
-       });
-    
-    py::class_<CS_VideoMode> cs_videomode(m, "_CS_VideoMode");
-    
-    py::class_<VideoMode, CS_VideoMode> videomode(m, "VideoMode");
-    videomode.doc() = "Video mode";
-    videomode
-      .def(py::init<cs::VideoMode::PixelFormat, int, int, int>(),
-           py::arg("pixelFormat"), py::arg("width"), py::arg("height"), py::arg("fps"))
-      .def_property("pixelFormat",
-            [](VideoMode &__inst) { return (cs::VideoMode::PixelFormat)__inst.pixelFormat; },
-            [](VideoMode &__inst, cs::VideoMode::PixelFormat v) { __inst.pixelFormat = v; })
-      .def_readwrite("width", &VideoMode::width)
-      .def_readwrite("height", &VideoMode::height)
-      .def_readwrite("fps", &VideoMode::fps);
-      
-    
-    py::enum_<VideoMode::PixelFormat>(videomode, "PixelFormat")
-      .value("kUnknown", VideoMode::PixelFormat::kUnknown)
-      .value("kMJPEG", VideoMode::PixelFormat::kMJPEG)
-      .value("kYUYV", VideoMode::PixelFormat::kYUYV)
-      .value("kRGB565", VideoMode::PixelFormat::kRGB565)
-      .value("kBGR", VideoMode::PixelFormat::kBGR)
-      .value("kGray", VideoMode::PixelFormat::kGray);
-    
-    #define def_status_fn(n, N, T, ...) def(n, [](T t) { \
-        py::gil_scoped_release __r; \
-        CS_Status s = 0; \
-        return N(t, &s); \
-    }, __VA_ARGS__)
-    
-    m.def("getNetworkInterfaces", &GetNetworkInterfaces, release_gil())
-     .def_status_fn("getHttpCameraUrls", GetHttpCameraUrls, CS_Source, py::arg("source"))
-     .def_status_fn("getUsbCameraPath", GetUsbCameraPath, CS_Source, py::arg("source"))
-     .def("getTelemetryElapsedTime", &GetTelemetryElapsedTime, release_gil())
-     .def("setTelemetryPeriod", &SetTelemetryPeriod, release_gil(),
-          py::arg("seconds"))
-     .def("setLogger", &SetLogger, py::arg("func"), py::arg("min_level"));
-
-    // cscore_oo.h
-
-    py::class_<VideoProperty> videoproperty(m, "VideoProperty");
-    videoproperty.doc() = "A source or sink property.";
-    videoproperty
-      .def("getName", &VideoProperty::GetName, release_gil())
-      .def("getKind", &VideoProperty::GetKind)
-      .def("isBoolean", &VideoProperty::IsBoolean)
-      .def("isInteger", &VideoProperty::IsInteger)
-      .def("isString", &VideoProperty::IsString)
-      .def("isEnum", &VideoProperty::IsEnum)
-      .def("get", &VideoProperty::Get, release_gil())
-      .def("set", &VideoProperty::Set, py::arg("value"), release_gil())
-      .def("getMin", &VideoProperty::GetMin, release_gil())
-      .def("getMax", &VideoProperty::GetMax, release_gil())
-      .def("getStep", &VideoProperty::GetStep, release_gil())
-      .def("getDefault", &VideoProperty::GetDefault, release_gil())
-      .def("getString", (std::string (VideoProperty::*)() const)&VideoProperty::GetString, release_gil())
-      /*.def("GetString", [](VideoProperty &__inst) {
-        wpi::SmallVectorImpl<char> buf;
-        auto __ret = __inst.GetString(buf);
-        return std::make_tuple(__ret, buf);
-      })*/
-      .def("setString", &VideoProperty::SetString, py::arg("value"), release_gil())
-      .def("getChoices", &VideoProperty::GetChoices, release_gil())
-      .def("getLastStatus", &VideoProperty::GetLastStatus);
-    
-    py::enum_<VideoProperty::Kind>(videoproperty, "Kind")
-      .value("kNone", VideoProperty::Kind::kNone)
-      .value("kBoolean", VideoProperty::Kind::kBoolean)
-      .value("kInteger", VideoProperty::Kind::kInteger)
-      .value("kString", VideoProperty::Kind::kString)
-      .value("kEnum", VideoProperty::Kind::kEnum);
-    
-    py::class_<VideoSource> videosource(m, "VideoSource");
-    videosource.doc() = "A source for video that provides a sequence of frames.";
-    videosource
-      .def(py::init<cs::VideoSource>(), py::arg("source"), release_gil())
-      .def("getHandle", &VideoSource::GetHandle)
-      .def(py::self == py::self)
-      .def(py::self != py::self)
-      .def("getKind", &VideoSource::GetKind, release_gil(),
-          "Get the kind of the source")
-      .def("getName", &VideoSource::GetName, release_gil(),
-          "Get the name of the source. The name is an arbitrary identifier " 
-          "provided when the source is created, and should be unique.")
-      .def("getDescription", &VideoSource::GetDescription, release_gil(),
-          "Get the source description.  This is source-kind specific.")
-      .def("getLastFrameTime", &VideoSource::GetLastFrameTime, release_gil(),
-          "Get the last time a frame was captured.")
-      .def("setConnectionStrategy", &VideoSource::SetConnectionStrategy, py::arg("strategy"), release_gil(),
-          "Set the connection strategy.  By default, the source will automatically "
-          "connect or disconnect based on whether any sinks are connected.\n\n"
-          ":param strategy: connection strategy (see ConnectionStrategy)")
-      .def("isConnected", &VideoSource::IsConnected, release_gil(),
-          "Is the source currently connected to whatever is providing the images?")
-      .def("isEnabled", &VideoSource::IsEnabled, release_gil(),
-          "Gets source enable status.  This is determined with a combination of\n"
-          "connection strategy and the number of sinks connected.")
-      .def("getProperty", &VideoSource::GetProperty, py::arg("name"), release_gil(),
-          "Get a property.\n\n"
-          ":param name: Property name\n"
-          ":returns: Property contents (VideoSource.Kind.kNone if no property with the given name exists)")
-      .def("enumerateProperties", &VideoSource::EnumerateProperties, release_gil(), "Enumerate all properties of this source")
-      .def("getVideoMode", &VideoSource::GetVideoMode, release_gil(), "Get the current video mode.")
-      .def("setVideoMode", [](VideoSource &__inst, VideoMode mode) {
-        py::gil_scoped_release __release;
-        return __inst.SetVideoMode(mode);
-    }, py::arg("mode"),
-        "Set the video mode.\n\n"
-        ":param mode: Video mode")
-      .def("setVideoMode", [](VideoSource &__inst, VideoMode::PixelFormat pixelFormat, int width, int height, int fps) {
-        py::gil_scoped_release __release;
-        return __inst.SetVideoMode(pixelFormat, width, height, fps);
-      }, py::arg("pixelFormat"), py::arg("width"), py::arg("height"), py::arg("fps"),
-          "Set the video mode.\n\n"
-          ":param pixelFormat: desired pixel format\n"
-          ":param width: desired width\n"
-          ":param height: desired height\n"
-          ":param fps: desired FPS\n"
-          ":returns: True if set successfully")
-      .def("setPixelFormat", &VideoSource::SetPixelFormat, py::arg("pixelFormat"), release_gil(),
-          "Set the pixel format.\n\n"
-          ":param pixelFormat: desired pixel format\n"
-          ":returns: True if set successfully")
-      .def("setResolution", &VideoSource::SetResolution, py::arg("width"), py::arg("height"), release_gil(),
-          "Set the resolution.\n\n"
-          ":param width: desired width\n"
-          ":param height: desired height\n"
-          ":returns: True if set successfully")
-      .def("setFPS", &VideoSource::SetFPS, py::arg("fps"), release_gil(),
-          "Set the frames per second (FPS).\n\n"
-          ":param fps: desired FPS\n"
-          ":returns: True if set successfully")
-      .def("setConfigJson", [](VideoSource &__inst, std::string_view config) {
-        py::gil_scoped_release __release;
-        return __inst.SetConfigJson(config);
-      }, py::arg("config"),
-        "Set video mode and properties from a JSON configuration string.\n\n"
-        ":param config: Configuration\n"
-        ":returns: True if set successfully")
-      .def("getConfigJson", &VideoSource::GetConfigJson, release_gil(),
-           "Get a JSON configuration string.\n\n"
-           ":returns: JSON string")
-      .def("getActualFPS", &VideoSource::GetActualFPS, release_gil(),
-           "Get the actual FPS.\n\n"
-           ":func:`.setTelemetryPeriod` must be called for this to be valid.\n\n"
-           ":returns: Actual FPS averaged over the telemetry period.")
-      .def("getActualDataRate", &VideoSource::GetActualDataRate, release_gil(),
-           "Get the data rate (in bytes per second).\n\n"
-           ":func:`.setTelemetryPeriod` must be called for this to be valid.\n\n"
-           ":returns: Data rate averaged over the telemetry period.")
-      .def("enumerateVideoModes", &VideoSource::EnumerateVideoModes, release_gil(),
-          "Enumerate all known video modes for this source.")
-      .def("getLastStatus", &VideoSource::GetLastStatus)
-      .def("enumerateSinks", &VideoSource::EnumerateSinks, release_gil(),
-          "Enumerate all sinks connected to this source.\n\n"
-          ":returns: list of sinks.")
-      .def_static("enumerateSources", &VideoSource::EnumerateSources, release_gil(),
-          "Enumerate all existing sources.\n\n"
-          ":returns: list of sources.");
-      //.def("__repr__", [](VideoSource &__inst){
-      //  std::stringstream id;
-      //  id << (void const *)&__inst;
-      //  return std::string("<VideoSource name=") + __inst.GetName() + " kind=" + std::to_string(__inst.GetKind()) + " at" + id.str() + ">";
-      //});
-    
-    py::enum_<VideoSource::Kind>(videosource, "Kind")
-      .value("kUnknown", VideoSource::Kind::kUnknown)
-      .value("kUsb", VideoSource::Kind::kUsb)
-      .value("kHttp", VideoSource::Kind::kHttp)
-      .value("kCv", VideoSource::Kind::kCv);
-    
-    py::enum_<VideoSource::ConnectionStrategy>(videosource, "ConnectionStrategy")
-      .value("kAutoManage", VideoSource::kConnectionAutoManage)
-      .value("kKeepOpen", VideoSource::kConnectionKeepOpen)
-      .value("kForceClose", VideoSource::kConnectionForceClose);
-    
-    py::class_<VideoCamera, VideoSource> videocamera(m, "VideoCamera");
-    videocamera.doc() = "A source that represents a video camera.";
-    videocamera
-      .def("setBrightness", &VideoCamera::SetBrightness, release_gil(),
-           py::arg("brightness"),
-           "Set the brightness, as a percentage (0-100).")
-      .def("getBrightness", &VideoCamera::GetBrightness, release_gil(),
-           "Get the brightness, as a percentage (0-100).")
-      .def("setWhiteBalanceAuto", &VideoCamera::SetWhiteBalanceAuto, release_gil(),
-           "Set the white balance to auto.")
-      .def("setWhiteBalanceHoldCurrent", &VideoCamera::SetWhiteBalanceHoldCurrent, release_gil(),
-           "Set the white balance to hold current.")
-      .def("setWhiteBalanceManual", &VideoCamera::SetWhiteBalanceManual, release_gil(),
-           py::arg("value"),
-           "Set the white balance to manual, with specified color temperature.")
-      .def("setExposureAuto", &VideoCamera::SetExposureAuto, release_gil(),
-           "Set the exposure to auto aperature.")
-      .def("setExposureHoldCurrent", &VideoCamera::SetExposureHoldCurrent, release_gil(),
-           "Set the exposure to hold current.")
-      .def("setExposureManual", &VideoCamera::SetExposureManual, release_gil(),
-           py::arg("value"),
-           "Set the exposure to manual, as a percentage (0-100).");
-    
-    py::enum_<VideoCamera::WhiteBalance>(videocamera, "WhiteBalance")
-      .value("kFixedIndoor", VideoCamera::WhiteBalance::kFixedIndoor)
-      .value("kFixedOutdoor1", VideoCamera::WhiteBalance::kFixedOutdoor1)
-      .value("kFixedOutdoor2", VideoCamera::WhiteBalance::kFixedOutdoor2)
-      .value("kFixedFluorescent1", VideoCamera::WhiteBalance::kFixedFluorescent1)
-      .value("kFixedFlourescent2", VideoCamera::WhiteBalance::kFixedFlourescent2);
-    
-    py::class_<UsbCamera, VideoCamera> usbcamera(m, "UsbCamera");
-    usbcamera.doc() = "A source that represents a USB camera.";
-    usbcamera
-      .def(py::init<std::string_view, int>(), release_gil(),
-          py::arg("name"), py::arg("dev"),
-          "Create a source for a USB camera based on device number.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param dev: Device number (e.g. 0 for ``/dev/video0``)")
-      .def(py::init<std::string_view, std::string_view>(), release_gil(),
-          py::arg("name"), py::arg("path"),
-          "Create a source for a USB camera based on device path.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param path: Path to device (e.g. ``/dev/video0`` on Linux)")
-      .def_static("enumerateUsbCameras", &UsbCamera::EnumerateUsbCameras, release_gil(),
-          "Enumerate USB cameras on the local system.\n\n"
-          ":returns: list of USB camera information (one for each camera)")
-      .def("setPath", &UsbCamera::SetPath, release_gil(), py::arg("path"),
-          "Change the path to the device.")
-      .def("getPath", &UsbCamera::GetPath, release_gil(), "Get the path to the device.")
-      .def("getInfo", &UsbCamera::GetInfo, release_gil(),
-          "Get the full camera information for the device.")
-      .def("setConnectVerbose", &UsbCamera::SetConnectVerbose, py::arg("level"),
-           "Set how verbose the camera connection messages are.\n\n"
-           ":param level: 0=don't display Connecting message, 1=do display message");
-
-    py::class_<HttpCamera, VideoCamera> httpcamera(m, "HttpCamera");
-    httpcamera.doc() = "A source that represents a MJPEG-over-HTTP (IP) camera.";
-    
-    // this has to come before the constructor definition
-    py::enum_<HttpCamera::HttpCameraKind>(httpcamera, "HttpCameraKind")
-      .value("kUnknown", HttpCamera::HttpCameraKind::kUnknown)
-      .value("kMJPGStreamer", HttpCamera::HttpCameraKind::kMJPGStreamer)
-      .value("kCSCore", HttpCamera::HttpCameraKind::kCSCore)
-      .value("kAxis", HttpCamera::HttpCameraKind::kAxis);
-    
-    httpcamera
-      //.def(py::init<std::string_view,std::string_view,HttpCamera::HttpCameraKind>())
-      //.def(py::init<std::string_view,const char *,cs::HttpCamera::HttpCameraKind>())
-      .def(py::init<std::string_view, std::string_view, cs::HttpCamera::HttpCameraKind>(), release_gil(),
-           py::arg("name"), py::arg("url"), py::arg("kind") = HttpCamera::HttpCameraKind::kUnknown,
-          "Create a source for a MJPEG-over-HTTP (IP) camera.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param url: Camera URL (e.g. \"http://10.x.y.11/video/stream.mjpg\")\n"
-          ":param kind: Camera kind (e.g. kAxis)")
-      .def(py::init<std::string_view, wpi::span<const std::string>, HttpCamera::HttpCameraKind>(), release_gil(),
-           py::arg("name"), py::arg("urls"), py::arg("kind") = HttpCamera::HttpCameraKind::kUnknown,
-          "Create a source for a MJPEG-over-HTTP (IP) camera.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param urls: Array of Camera URLs\n"
-          ":param kind: Camera kind (e.g. kAxis)")
-      //.def(py::init<std::string_view,std::initializer_list<T>,cs::HttpCamera::HttpCameraKind>())
-      .def("getHttpCameraKind", &HttpCamera::GetHttpCameraKind, release_gil(),
-          "Get the kind of HTTP camera. "
-          "Autodetection can result in returning a different value than the camera was created with.")
-      .def("setUrls", (void (HttpCamera::*)(wpi::span<const std::string>))&HttpCamera::SetUrls, release_gil(),
-           py::arg("urls"),
-           "Change the URLs used to connect to the camera.")
-      //.def("SetUrls", (void (HttpCamera::*)(std::initializer_list<T>))&HttpCamera::SetUrls)
-      .def("getUrls", &HttpCamera::GetUrls, release_gil(),
-           "Get the URLs used to connect to the camera.");
-    
-    py::class_<AxisCamera, HttpCamera> axiscamera(m, "AxisCamera");
-    axiscamera.doc() = "A source that represents an Axis IP camera.";
-    axiscamera
-      .def(py::init<std::string_view, std::string_view>(), release_gil(),
-          py::arg("name"), py::arg("host"),
-          "Create a source for a MJPEG-over-HTTP (IP) camera.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param host: Camera host IP or DNS name (e.g. \"10.x.x.11\")\n"
-          ":param kind: Camera kind (e.g. kAxis)")
-      //.def(py::init<std::string_view,const char *>(),
-      //.def(py::init<std::string_view,std::string>(),
-      .def(py::init<std::string_view, wpi::span<const std::string>>(), release_gil(),
-          py::arg("name"), py::arg("hosts"),
-          "Create a source for a MJPEG-over-HTTP (IP) camera.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param hosts: Array of Camera host IPs/DNS names\n"
-          ":param kind: Camera kind (e.g. kAxis)");
-      //.def(py::init<std::string_view,std::initializer_list<T>>());
-    
-    py::class_<ImageSource, VideoSource> imagesource(m, "ImageSource");
-    imagesource.doc() = "A base class for single image providing sources.";
-    imagesource
-      .def("notifyError", &ImageSource::NotifyError, release_gil(),
-          py::arg("msg"),
-          "Signal sinks that an error has occurred.  This should be called instead "
-          "of :meth:`notifyFrame` when an error occurs.")
-      .def("setConnected", &ImageSource::SetConnected, release_gil(),
-          py::arg("connected"),
-          "Set source connection status.  Defaults to true.\n\n"
-          ":param connected: True for connected, false for disconnected")
-      .def("setDescription", &ImageSource::SetDescription, release_gil(),
-          py::arg("description"),
-          "Set source description.\n\n"
-          ":param description: Description")
-      .def("createProperty", &ImageSource::CreateProperty, release_gil(),
-          py::arg("name"), py::arg("kind"), py::arg("minimum"), py::arg("maximum"), py::arg("step"), py::arg("defaultValue"), py::arg("value"),
-          "Create a property.\n\n"
-          ":param name: Property name\n"
-          ":param kind: Property kind\n"
-          ":param minimum: Minimum value\n"
-          ":param maximum: Maximum value\n"
-          ":param step: Step value\n"
-          ":param defaultValue: Default value\n"
-          ":param value: Current value\n\n"
-          ":returns: Property\n")
-      .def("createIntegerProperty", &ImageSource::CreateIntegerProperty, release_gil(),
-          py::arg("name"), py::arg("minimum"), py::arg("maximum"), py::arg("step"), py::arg("defaultValue"), py::arg("value"),
-          "Create a property.\n\n"
-          ":param name: Property name\n"
-          ":param minimum: Minimum value\n"
-          ":param maximum: Maximum value\n"
-          ":param step: Step value\n"
-          ":param defaultValue: Default value\n"
-          ":param value: Current value\n\n"
-          ":returns: Property\n")
-      .def("createBooleanProperty", &ImageSource::CreateBooleanProperty, release_gil(),
-          py::arg("name"), py::arg("defaultValue"), py::arg("value"),
-          "Create a property.\n\n"
-          ":param name: Property name\n"
-          ":param defaultValue: Default value\n"
-          ":param value: Current value\n\n"
-          ":returns: Property\n")
-      .def("createStringProperty", &ImageSource::CreateStringProperty, release_gil(),
-          py::arg("name"), py::arg("value"),
-          "Create a property.\n\n"
-          ":param name: Property name\n"
-          ":param value: Current value\n\n"
-          ":returns: Property\n")
-      .def("setEnumPropertyChoices", (void (ImageSource::*)(const VideoProperty &property, wpi::span<const std::string> choices))&ImageSource::SetEnumPropertyChoices, release_gil(),
-          py::arg("property"), py::arg("choices"),
-          "Configure enum property choices.\n\n"
-          ":param property: Property\n"
-          ":param choices: Choices");
-      /*.def("SetEnumPropertyChoices", [](ImageSource &__inst, std::initializer_list<T> choices) {
-        cs::VideoProperty property;
-        auto __ret = __inst.SetEnumPropertyChoices(property, choices);
-        return std::make_tuple(__ret, property);
-    });*/
-
-    py::class_<VideoSink> videosink(m, "VideoSink");
-    videosink.doc() = "A sink for video that accepts a sequence of frames.";
-    videosink
-      .def(py::init<cs::VideoSink>(), release_gil(), py::arg("sink"))
-      .def("getHandle", &VideoSink::GetHandle)
-      .def(py::self == py::self)
-      .def(py::self != py::self)
-      .def("getKind", &VideoSink::GetKind, release_gil(),
-          "Get the kind of the sink.")
-      .def("getName", &VideoSink::GetName, release_gil(),
-          "Get the name of the sink.  The name is an arbitrary identifier "
-          "provided when the sink is created, and should be unique.")
-      .def("getDescription", &VideoSink::GetDescription, release_gil(),
-          "Get the sink description.  This is sink-kind specific.")
-      .def("getProperty", &VideoSink::GetProperty, py::arg("name"), release_gil(),
-          "Get a property.\n\n"
-          ":param name: Property name\n"
-          ":returns: Property contents (VideoSource.Kind.kNone if no property with the given name exists)")
-      .def("enumerateProperties", &VideoSink::EnumerateProperties, release_gil(),
-          "Enumerate all properties of this sink")
-      .def("setConfigJson", [](VideoSink &__inst, std::string_view config) {
-        py::gil_scoped_release __release;
-        return __inst.SetConfigJson(config);
-      }, py::arg("config"),
-        "Set properties from a JSON configuration string.\n\n"
-        "The format of the JSON input is::\n\n"
-        "    {\n"
-        "      \"properties\": [\n"
-        "        {\n"
-        "          \"name\": \"property name\",\n"
-        "          \"value\": \"property value\"\n"
-        "         }\n"
-        "      ]\n"
-        "    }\n\n"
-        ":param config: configuration\n"
-        ":returns: True if set successfully")
-      .def("getConfigJson", &VideoSink::GetConfigJson, release_gil(),
-          "Get a JSON configuration string.\n\n"
-          ":returns: JSON configuration string\n")
-      .def("setSource", &VideoSink::SetSource, release_gil(),
-          py::arg("source"),
-          "Configure which source should provide frames to this sink.  Each sink "
-          "can accept frames from only a single source, but a single source can "
-          "provide frames to multiple clients.\n\n"
-          ":param source: Source")
-      .def("getSource", &VideoSink::GetSource, release_gil(),
-          "Get the connected source.\n\n"
-          ":returns: Connected source (empty if none connected).")
-      .def("getSourceProperty", &VideoSink::GetSourceProperty, release_gil(),
-          py::arg("name"),
-          "Get a property of the associated source.\n\n"
-          ":param name: Property name\n"
-          ":returns: Property (VideoSink.Kind.kNone if no property with the given name exists or no source connected)")
-      .def("getLastStatus", &VideoSink::GetLastStatus)
-      .def_static("enumerateSinks", &VideoSink::EnumerateSinks, release_gil(),
-          "Enumerate all existing sinks.\n\n"
-          ":returns: list of sinks.");
-    
-    py::enum_<VideoSink::Kind>(videosink, "Kind")
-      .value("kUnknown", VideoSink::Kind::kUnknown)
-      .value("kMjpeg", VideoSink::Kind::kMjpeg)
-      .value("kCv", VideoSink::Kind::kCv);
-    
-    py::class_<MjpegServer, VideoSink> mjpegserver(m, "MjpegServer");
-    mjpegserver.doc() = "A sink that acts as a MJPEG-over-HTTP network server.";
-    mjpegserver
-      .def(py::init<std::string_view, std::string_view, int>(), release_gil(),
-          py::arg("name"), py::arg("listenAddress"), py::arg("port"),
-          "Create a MJPEG-over-HTTP server sink.\n\n"
-          ":param name: Sink name (arbitrary unique identifier)\n"
-          ":param listenAddress: TCP listen address (empty string for all addresses)\n"
-          ":param port: TCP port number")
-      .def(py::init<std::string_view, int>(), release_gil(),
-          py::arg("name"), py::arg("port"),
-          "Create a MJPEG-over-HTTP server sink.\n\n"
-          ":param name: Sink name (arbitrary unique identifier)\n"
-          ":param port: TCP port number")
-      .def("getListenAddress", &MjpegServer::GetListenAddress, "Get the listen address of the server.")
-      .def("getPort", &MjpegServer::GetPort, "Get the port number of the server.")
-      .def("setResolution", &MjpegServer::SetResolution, py::arg("width"), py::arg("height"), release_gil(),
-          "Set the stream resolution for clients that don't specify it.\n\n"
-          "It is not necessary to set this if it is the same as the source "
-          "resolution.\n\n"
-          "Setting this different than the source resolution will result in "
-          "increased CPU usage, particularly for MJPEG source cameras, as it will "
-          "decompress, resize, and recompress the image, instead of using the "
-          "camera's MJPEG image directly.\n\n"
-          ":param width:  width, 0 for unspecified\n"
-          ":param height: height, 0 for unspecified")
-      .def("setFPS", &MjpegServer::SetFPS, py::arg("fps"), release_gil(),
-          "Set the stream frames per second (FPS) for clients that don't specify it.\n\n"
-          "It is not necessary to set this if it is the same as the source FPS.\n\n"
-          ":param fps: FPS, 0 for unspecified")
-      .def("setCompression", &MjpegServer::SetCompression, py::arg("quality"), release_gil(),
-          "Set the compression for clients that don't specify it.\n\n"
-          "Setting this will result in increased CPU usage for MJPEG source cameras "
-          "as it will decompress and recompress the image instead of using the "
-          "camera's MJPEG image directly.\n\n"
-          ":param quality: JPEG compression quality (0-100), -1 for unspecified")
-      .def("setDefaultCompression", &MjpegServer::SetDefaultCompression, py::arg("quality"), release_gil(),
-           "Set the default compression used for non-MJPEG sources.  If not set, "
-           "80 is used.  This function has no effect on MJPEG source cameras; use "
-           "setCompression() instead to force recompression of MJPEG source images.\n\n"
-           ":param quality: JPEG compression quality (0-100)");
-    
-    py::class_<ImageSink, VideoSink> imagesink(m, "ImageSink");
-    imagesink.doc() = "A base class for single image reading sinks.";
-    imagesink
-      .def("setDescription", &ImageSink::SetDescription, release_gil(),
-          py::arg("description"),
-          "Set sink description.\n\n"
-          ":param description: Description")
-      .def("getError", &ImageSink::GetError,
-           "Get error string.  Call this if :meth:`waitForFrame` returns 0 to determine what the error is.")
-      .def("setEnabled", &ImageSink::SetEnabled, release_gil(),
-          py::arg("enabled"),
-          "Enable or disable getting new frames.\n"
-          "Disabling will cause processFrame (for callback-based ImageSinks) to not"
-          " be called and :meth:`waitForFrame` to not return.  This can be used to save"
-          " processor resources when frames are not needed.");
-    
-    py::class_<RawEvent> rawevent(m, "RawEvent");
-    rawevent.doc() = "Listener event";
-    rawevent
-      .def_readonly("kind", &RawEvent::kind)
-      .def_readonly("mode", &RawEvent::mode)
-      .def_readonly("name", &RawEvent::name)
-      .def_readonly("value", &RawEvent::value)
-      .def_readonly("valueStr", &RawEvent::valueStr)
-      .def_readonly("sourceHandle", &RawEvent::sourceHandle)
-      .def_readonly("sinkHandle", &RawEvent::sinkHandle);
-    
-    py::enum_<RawEvent::Kind>(rawevent, "Kind")
-      .value("kSourceCreated", RawEvent::Kind::kSourceCreated)
-      .value("kSourceDestroyed", RawEvent::Kind::kSourceDestroyed)
-      .value("kSourceConnected", RawEvent::Kind::kSourceConnected)
-      .value("kSourceDisconnected", RawEvent::Kind::kSourceDisconnected)
-      .value("kSourceVideoModesUpdated", RawEvent::Kind::kSourceVideoModesUpdated)
-      .value("kSourceVideoModeChanged", RawEvent::Kind::kSourceVideoModeChanged)
-      .value("kSourcePropertyCreated", RawEvent::Kind::kSourcePropertyCreated)
-      .value("kSourcePropertyValueUpdated", RawEvent::Kind::kSourcePropertyValueUpdated)
-      .value("kSourcePropertyChoicesUpdated", RawEvent::Kind::kSourcePropertyChoicesUpdated)
-      .value("kSinkSourceChanged", RawEvent::Kind::kSinkSourceChanged)
-      .value("kSinkCreated", RawEvent::Kind::kSinkCreated)
-      .value("kSinkDestroyed", RawEvent::Kind::kSinkDestroyed)
-      .value("kSinkEnabled", RawEvent::Kind::kSinkEnabled)
-      .value("kSinkDisabled", RawEvent::Kind::kSinkDisabled)
-      .value("kNetworkInterfacesChanged", RawEvent::Kind::kNetworkInterfacesChanged)
-      .value("kTelemetryUpdated", RawEvent::Kind::kTelemetryUpdated)
-      .value("kSinkPropertyCreated", RawEvent::Kind::kSinkPropertyCreated)
-      .value("kSinkPropertyValueUpdated", RawEvent::Kind::kSinkPropertyValueUpdated)
-      .value("kSinkPropertyChoicesUpdated", RawEvent::Kind::kSinkPropertyChoicesUpdated);
-    
-    py::class_<VideoEvent, RawEvent> videoevent(m, "VideoEvent");
-    videoevent.doc() = "An event generated by the library and provided to event listeners.";
-    videoevent
-      .def("getSource", &VideoEvent::GetSource)
-      .def("getSink", &VideoEvent::GetSink)
-      .def("getProperty", &VideoEvent::GetProperty);
-      
-    
-    py::class_<VideoListenerWrapper> videolistener(m, "VideoListener");
-    videolistener.doc() = 
-        "An event listener.  This calls back to a desigated callback function when\n"
-        "an event matching the specified mask is generated by the library.";
-    videolistener
-      .def(py::init<std::function<void(const VideoEvent&)>,int,bool>(), release_gil(),
-          py::arg("callback"), py::arg("eventMask"), py::arg("immediateNotify"),
-          "Create an event listener.\n\n"
-          ":param callback: Callback function\n"
-          ":param eventMask: Bitmask of VideoEvent.Kind values\n"
-          ":param immediateNotify: Whether callback should be immediately called with"
-          " a representative set of events for the current library state.");
-
-    // now in cscore_cv.h
-
-    py::class_<CvSource, ImageSource> cvsource(m, "CvSource");
-    cvsource.doc() = "A source for user code to provide OpenCV images as video frames.";
-    cvsource
-      .def(py::init<std::string_view, VideoMode>(), release_gil(),
-          py::arg("name"), py::arg("mode"),
-          "Create an OpenCV source.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param mode: Video mode being generated")
-      .def(py::init<std::string_view, VideoMode::PixelFormat, int, int, int>(), release_gil(),
-          py::arg("name"), py::arg("pixelFormat"), py::arg("width"), py::arg("height"), py::arg("fps"),
-          "Create an OpenCV source.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param pixelFormat: Pixel format\n"
-          ":param width: width\n"
-          ":param height: height\n"
-          ":param fps: fps")
-      .def("putFrame", &CvSource::PutFrame, release_gil(),
-          py::arg("image"),
-          "Put an OpenCV image and notify sinks.\n\n"
-          "Only 8-bit single-channel or 3-channel (with BGR channel order) images "
-          "are supported. If the format, depth or channel order is different, use "
-          "``cv2.convertTo()`` and/or ``cv2.cvtColor()`` to convert it first.\n\n"
-          ":param image: OpenCV image");
-
-    py::class_<CvSink, ImageSink> cvsink(m, "CvSink");
-    cvsink.doc() = "A sink for user code to accept video frames as OpenCV images.";
-    cvsink
-      .def(py::init<std::string_view>(), release_gil(),
-          py::arg("name"),
-          "Create a sink for accepting OpenCV images. "
-          ":meth:`grabFrame` must be called on the created sink to get each new image\n\n"
-          ":param name: Source name (arbitrary unique identifier)")
-      /*.def(py::init<std::string_view,std::function<void(uint64_t)>>(),
-          py::arg("name"), py::arg("processFrame"),
-          "Create a sink for accepting OpenCV images in a separate thread. "
-          "A thread will be created that calls the "
-          "``processFrame`` callback each time a new frame arrives.\n\n"
-          ":param name: Source name (arbitrary unique identifier)\n"
-          ":param processFrame: Frame processing function; will be called with a "
-          "time=0 if an error occurred.  processFrame should call :meth:`grabFrame`"
-          "or :meth:`getError` as needed.")*/
-      .def("setDescription", &CvSink::SetDescription, release_gil(),
-          py::arg("description"),
-          "Set sink description.\n\n"
-          ":param description: Description")
-      .def("grabFrame", [](CvSink &__inst, cv::Mat &image, double timeout) {
-        py::gil_scoped_release release;
-        auto __ret = __inst.GrabFrame(image, timeout);
-        return std::make_tuple(__ret, image);
-      }, py::arg("image"), py::arg("timeout") = 0.225,
-          "Wait for the next frame and get the image. Times out (returning 0) after timeout seconds.\n"
-          "The provided image will have three 8-bit channels stored in BGR order.\n\n"
-          ":returns: Frame time, or 0 on error (call :meth:`getError` to obtain the error message), returned image\n"
-          "          The frame time is in 1us increments"
-      )
-      .def("grabFrameNoTimeout", [](CvSink &__inst, cv::Mat &image) {
-          py::gil_scoped_release release;
-          auto __ret = __inst.GrabFrameNoTimeout(image);
-          return std::make_tuple(__ret, image);
-        }, py::arg("image"),
-          "Wait for the next frame and get the image. May block forever.\n"
-          "The provided image will have three 8-bit channels stored in BGR order.\n\n"
-          ":returns: Frame time, or 0 on error (call :meth:`getError` to obtain the error message), returned image\n"
-          "          The frame time is in 1us increments"
-        );
-}
diff --git a/src/ndarray_converter.cpp b/src/ndarray_converter.cpp
deleted file mode 100644
index 9283b50..0000000
--- a/src/ndarray_converter.cpp
+++ /dev/null
@@ -1,356 +0,0 @@
-// borrowed in spirit from https://github.com/yati-sagade/opencv-ndarray-conversion
-// MIT License
-
-#include "ndarray_converter.h"
-
-#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
-#include <numpy/ndarrayobject.h>
-
-#if PY_VERSION_HEX >= 0x03000000
-    #define PyInt_Check PyLong_Check
-    #define PyInt_AsLong PyLong_AsLong
-#endif
-
-#if CV_VERSION_MAJOR < 4
-using AccessFlag = int;
-#endif
-
-struct Tmp {
-    const char * name;
-
-    Tmp(const char * name ) : name(name) {}
-};
-
-Tmp info("return value");
-
-bool NDArrayConverter::init_numpy() {
-    // this has to be in this file, since PyArray_API is defined as static
-    import_array1(false);
-    return true;
-}
-
-/*
- * The following conversion functions are taken/adapted from OpenCV's cv2.cpp file
- * inside modules/python/src2 folder (OpenCV 3.1.0)
- */
-
-static PyObject* opencv_error = 0;
-
-static int failmsg(const char *fmt, ...)
-{
-    char str[1000];
-
-    va_list ap;
-    va_start(ap, fmt);
-    vsnprintf(str, sizeof(str), fmt, ap);
-    va_end(ap);
-
-    PyErr_SetString(PyExc_TypeError, str);
-    return 0;
-}
-
-class PyAllowThreads
-{
-public:
-    PyAllowThreads() : _state(PyEval_SaveThread()) {}
-    ~PyAllowThreads()
-    {
-        PyEval_RestoreThread(_state);
-    }
-private:
-    PyThreadState* _state;
-};
-
-class PyEnsureGIL
-{
-public:
-    PyEnsureGIL() : _state(PyGILState_Ensure()) {}
-    ~PyEnsureGIL()
-    {
-        PyGILState_Release(_state);
-    }
-private:
-    PyGILState_STATE _state;
-};
-
-#define ERRWRAP2(expr) \
-try \
-{ \
-    PyAllowThreads allowThreads; \
-    expr; \
-} \
-catch (const cv::Exception &e) \
-{ \
-    PyErr_SetString(opencv_error, e.what()); \
-    return 0; \
-}
-
-using namespace cv;
-
-class NumpyAllocator : public MatAllocator
-{
-public:
-    NumpyAllocator() { stdAllocator = Mat::getStdAllocator(); }
-    ~NumpyAllocator() {}
-
-    UMatData* allocate(PyObject* o, int dims, const int* sizes, int type, size_t* step) const
-    {
-        UMatData* u = new UMatData(this);
-        u->data = u->origdata = (uchar*)PyArray_DATA((PyArrayObject*) o);
-        npy_intp* _strides = PyArray_STRIDES((PyArrayObject*) o);
-        for( int i = 0; i < dims - 1; i++ )
-            step[i] = (size_t)_strides[i];
-        step[dims-1] = CV_ELEM_SIZE(type);
-        u->size = sizes[0]*step[0];
-        u->userdata = o;
-        return u;
-    }
-
-    UMatData* allocate(int dims0, const int* sizes, int type, void* data, size_t* step, AccessFlag flags, UMatUsageFlags usageFlags) const
-    {
-        if( data != 0 )
-        {
-            CV_Error(Error::StsAssert, "The data should normally be NULL!");
-            // probably this is safe to do in such extreme case
-            return stdAllocator->allocate(dims0, sizes, type, data, step, flags, usageFlags);
-        }
-        PyEnsureGIL gil;
-
-        int depth = CV_MAT_DEPTH(type);
-        int cn = CV_MAT_CN(type);
-        const int f = (int)(sizeof(size_t)/8);
-        int typenum = depth == CV_8U ? NPY_UBYTE : depth == CV_8S ? NPY_BYTE :
-        depth == CV_16U ? NPY_USHORT : depth == CV_16S ? NPY_SHORT :
-        depth == CV_32S ? NPY_INT : depth == CV_32F ? NPY_FLOAT :
-        depth == CV_64F ? NPY_DOUBLE : f*NPY_ULONGLONG + (f^1)*NPY_UINT;
-        int i, dims = dims0;
-        cv::AutoBuffer<npy_intp> _sizes(dims + 1);
-        for( i = 0; i < dims; i++ )
-            _sizes[i] = sizes[i];
-        if( cn > 1 )
-            _sizes[dims++] = cn;
-        PyObject* o = PyArray_SimpleNew(dims, _sizes, typenum);
-        if(!o)
-            CV_Error_(Error::StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims));
-        return allocate(o, dims0, sizes, type, step);
-    }
-
-    bool allocate(UMatData* u, AccessFlag accessFlags, UMatUsageFlags usageFlags) const
-    {
-        return stdAllocator->allocate(u, accessFlags, usageFlags);
-    }
-
-    void deallocate(UMatData* u) const
-    {
-        if(!u)
-            return;
-        PyEnsureGIL gil;
-        CV_Assert(u->urefcount >= 0);
-        CV_Assert(u->refcount >= 0);
-        if(u->refcount == 0)
-        {
-            PyObject* o = (PyObject*)u->userdata;
-            Py_XDECREF(o);
-            delete u;
-        }
-    }
-
-    const MatAllocator* stdAllocator;
-};
-
-NumpyAllocator g_numpyAllocator;
-
-bool NDArrayConverter::toMat(PyObject *o, Mat &m)
-{
-    bool allowND = true;
-    if(!o || o == Py_None)
-    {
-        if( !m.data )
-            m.allocator = &g_numpyAllocator;
-        return true;
-    }
-
-    if( PyInt_Check(o) )
-    {
-        double v[] = {static_cast<double>(PyInt_AsLong((PyObject*)o)), 0., 0., 0.};
-        m = Mat(4, 1, CV_64F, v).clone();
-        return true;
-    }
-    if( PyFloat_Check(o) )
-    {
-        double v[] = {PyFloat_AsDouble((PyObject*)o), 0., 0., 0.};
-        m = Mat(4, 1, CV_64F, v).clone();
-        return true;
-    }
-    if( PyTuple_Check(o) )
-    {
-        int i, sz = (int)PyTuple_Size((PyObject*)o);
-        m = Mat(sz, 1, CV_64F);
-        for( i = 0; i < sz; i++ )
-        {
-            PyObject* oi = PyTuple_GET_ITEM(o, i);
-            if( PyInt_Check(oi) )
-                m.at<double>(i) = (double)PyInt_AsLong(oi);
-            else if( PyFloat_Check(oi) )
-                m.at<double>(i) = (double)PyFloat_AsDouble(oi);
-            else
-            {
-                failmsg("%s is not a numerical tuple", info.name);
-                m.release();
-                return false;
-            }
-        }
-        return true;
-    }
-
-    if( !PyArray_Check(o) )
-    {
-        failmsg("%s is not a numpy array, neither a scalar", info.name);
-        return false;
-    }
-
-    PyArrayObject* oarr = (PyArrayObject*) o;
-
-    bool needcopy = false, needcast = false;
-    int typenum = PyArray_TYPE(oarr), new_typenum = typenum;
-    int type = typenum == NPY_UBYTE ? CV_8U :
-               typenum == NPY_BYTE ? CV_8S :
-               typenum == NPY_USHORT ? CV_16U :
-               typenum == NPY_SHORT ? CV_16S :
-               typenum == NPY_INT ? CV_32S :
-               typenum == NPY_INT32 ? CV_32S :
-               typenum == NPY_FLOAT ? CV_32F :
-               typenum == NPY_DOUBLE ? CV_64F : -1;
-
-    if( type < 0 )
-    {
-        if( typenum == NPY_INT64 || typenum == NPY_UINT64 || typenum == NPY_LONG )
-        {
-            needcopy = needcast = true;
-            new_typenum = NPY_INT;
-            type = CV_32S;
-        }
-        else
-        {
-            failmsg("%s data type = %d is not supported", info.name, typenum);
-            return false;
-        }
-    }
-
-#ifndef CV_MAX_DIM
-    const int CV_MAX_DIM = 32;
-#endif
-
-    int ndims = PyArray_NDIM(oarr);
-    if(ndims >= CV_MAX_DIM)
-    {
-        failmsg("%s dimensionality (=%d) is too high", info.name, ndims);
-        return false;
-    }
-
-    int size[CV_MAX_DIM+1];
-    size_t step[CV_MAX_DIM+1];
-    size_t elemsize = CV_ELEM_SIZE1(type);
-    const npy_intp* _sizes = PyArray_DIMS(oarr);
-    const npy_intp* _strides = PyArray_STRIDES(oarr);
-    bool ismultichannel = ndims == 3 && _sizes[2] <= CV_CN_MAX;
-
-    for( int i = ndims-1; i >= 0 && !needcopy; i-- )
-    {
-        // these checks handle cases of
-        //  a) multi-dimensional (ndims > 2) arrays, as well as simpler 1- and 2-dimensional cases
-        //  b) transposed arrays, where _strides[] elements go in non-descending order
-        //  c) flipped arrays, where some of _strides[] elements are negative
-        // the _sizes[i] > 1 is needed to avoid spurious copies when NPY_RELAXED_STRIDES is set
-        if( (i == ndims-1 && _sizes[i] > 1 && (size_t)_strides[i] != elemsize) ||
-            (i < ndims-1 && _sizes[i] > 1 && _strides[i] < _strides[i+1]) )
-            needcopy = true;
-    }
-
-    if( ismultichannel && _strides[1] != (npy_intp)elemsize*_sizes[2] )
-        needcopy = true;
-
-    if (needcopy)
-    {
-        //if (info.outputarg)
-        //{
-        //    failmsg("Layout of the output array %s is incompatible with cv::Mat (step[ndims-1] != elemsize or step[1] != elemsize*nchannels)", info.name);
-        //    return false;
-        //}
-
-        if( needcast ) {
-            o = PyArray_Cast(oarr, new_typenum);
-            oarr = (PyArrayObject*) o;
-        }
-        else {
-            oarr = PyArray_GETCONTIGUOUS(oarr);
-            o = (PyObject*) oarr;
-        }
-
-        _strides = PyArray_STRIDES(oarr);
-    }
-
-    // Normalize strides in case NPY_RELAXED_STRIDES is set
-    size_t default_step = elemsize;
-    for ( int i = ndims - 1; i >= 0; --i )
-    {
-        size[i] = (int)_sizes[i];
-        if ( size[i] > 1 )
-        {
-            step[i] = (size_t)_strides[i];
-            default_step = step[i] * size[i];
-        }
-        else
-        {
-            step[i] = default_step;
-            default_step *= size[i];
-        }
-    }
-
-    // handle degenerate case
-    if( ndims == 0) {
-        size[ndims] = 1;
-        step[ndims] = elemsize;
-        ndims++;
-    }
-
-    if( ismultichannel )
-    {
-        ndims--;
-        type |= CV_MAKETYPE(0, size[2]);
-    }
-
-    if( ndims > 2 && !allowND )
-    {
-        failmsg("%s has more than 2 dimensions", info.name);
-        return false;
-    }
-
-    m = Mat(ndims, size, type, PyArray_DATA(oarr), step);
-    m.u = g_numpyAllocator.allocate(o, ndims, size, type, step);
-    m.addref();
-
-    if( !needcopy )
-    {
-        Py_INCREF(o);
-    }
-    m.allocator = &g_numpyAllocator;
-
-    return true;
-}
-
-PyObject* NDArrayConverter::toNDArray(const cv::Mat& m)
-{
-    if( !m.data )
-        Py_RETURN_NONE;
-    Mat temp, *p = (Mat*)&m;
-    if(!p->u || p->allocator != &g_numpyAllocator)
-    {
-        temp.allocator = &g_numpyAllocator;
-        ERRWRAP2(m.copyTo(temp));
-        p = &temp;
-    }
-    PyObject* o = (PyObject*)p->u->userdata;
-    Py_INCREF(o);
-    return o;
-}
diff --git a/src/ndarray_converter.h b/src/ndarray_converter.h
deleted file mode 100644
index da5ea16..0000000
--- a/src/ndarray_converter.h
+++ /dev/null
@@ -1,42 +0,0 @@
-# ifndef __NDARRAY_CONVERTER_H__
-# define __NDARRAY_CONVERTER_H__
-
-#include <Python.h>
-#include <opencv2/core/core.hpp>
-
-
-class NDArrayConverter {
-public:
-    // must call this first, or the other routines don't work!
-    static bool init_numpy();
-    
-    static bool toMat(PyObject* o, cv::Mat &m);
-    static PyObject* toNDArray(const cv::Mat& mat);
-};
-
-//
-// Define the type converter
-//
-
-#include <pybind11/pybind11.h>
-
-namespace pybind11 { namespace detail {
-    
-template <> struct type_caster<cv::Mat> {
-public:
-    
-    PYBIND11_TYPE_CASTER(cv::Mat, _("numpy.ndarray"));
-    
-    bool load(handle src, bool) {
-        return NDArrayConverter::toMat(src.ptr(), value);
-    }
-    
-    static handle cast(const cv::Mat &m, return_value_policy, handle defval) {
-        return handle(NDArrayConverter::toNDArray(m));
-    }
-};
-    
-    
-}} // namespace pybind11::detail
-
-# endif
diff --git a/src/wpiutil_converters.hpp b/src/wpiutil_converters.hpp
deleted file mode 100644
index 16a2c41..0000000
--- a/src/wpiutil_converters.hpp
+++ /dev/null
@@ -1,47 +0,0 @@
-
-#include <pybind11/pybind11.h>
-
-
-namespace pybind11 { namespace detail {
-    
-template <typename Type> struct type_caster<wpi::span<Type>> {
-  using value_conv = make_caster<Type>;
-  using value_type = typename std::remove_cv<Type>::type;
-  PYBIND11_TYPE_CASTER(wpi::span<Type>, _("List[") + value_conv::name + _("]"));
-
-  wpi::SmallVector<value_type, 32> vec;
-  bool load(handle src, bool convert) {
-    if (!isinstance<sequence>(src) || isinstance<str>(src))
-      return false;
-    auto s = reinterpret_borrow<sequence>(src);
-    vec.reserve(s.size());
-    for (auto it : s) {
-      value_conv conv;
-      if (!conv.load(it, convert))
-        return false;
-      vec.push_back(cast_op<Type &&>(std::move(conv)));
-    }
-    value = wpi::span<Type>(std::data(vec), std::size(vec));
-    return true;
-  }
-
-public:
-  template <typename T>
-  static handle cast(T &&src, return_value_policy policy, handle parent) {
-    if (!std::is_lvalue_reference<T>::value)
-      policy = return_value_policy_override<Type>::policy(policy);
-    list l(src.size());
-    size_t index = 0;
-    for (auto &&value : src) {
-      auto value_ = reinterpret_steal<object>(
-          value_conv::cast(forward_like<T>(value), policy, parent));
-      if (!value_)
-        return handle();
-      PyList_SET_ITEM(l.ptr(), (ssize_t)index++,
-                      value_.release().ptr()); // steals a reference
-    }
-    return l.release();
-  }
-};
-        
-}} // namespace pybind11::detail


Test patch:
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000..e079f8a
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,1 @@
+pytest
diff --git a/tests/run_tests.py b/tests/run_tests.py
new file mode 100755
index 0000000..ad62d89
--- /dev/null
+++ b/tests/run_tests.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+
+import os
+from os.path import abspath, dirname
+import sys
+import subprocess
+
+if __name__ == "__main__":
+
+    root = abspath(dirname(__file__))
+    os.chdir(root)
+
+    subprocess.check_call([sys.executable, "-m", "pytest"])
diff --git a/tests/test_import.py b/tests/test_import.py
new file mode 100644
index 0000000..c3bbbb8
--- /dev/null
+++ b/tests/test_import.py
@@ -0,0 +1,5 @@
+import cscore
+
+
+def test_cscore_import():
+    pass