npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@leverege/ptz-camera

v0.3.0

Published

JS port of py-ptz-camera (geometric modeling for PTZ cameras: kinematics, projection, frustums).

Readme

@leverege/ptz-camera

JS port of py-ptz-camera — geometric modeling for PTZ cameras: kinematic chain, forward projection (3D → pixel), inverse kinematics (world point → pan/tilt/zoom), FOV computation, and frustum geometry for visualization.

The Python sister package owns the calibration optimizer (PTZCalibrationParameters, calibrate(...)); this JS port covers the geometric-modeling half only — what a viewer needs to render, project, and reason about a configured camera. Wire-format dicts produced by to_dict() on either side are interchangeable.

Installation

npm install @leverege/ptz-camera

Quick start

import {
  PTZCamera, PTZIntrinsics, Angle, Transformation
} from '@leverege/ptz-camera'

// 1. Define the camera hardware (intrinsics).
const ident = Transformation.identity()
const intrinsics = new PTZIntrinsics({
  sensorWidthMm: 6.28, sensorHeightMm: 4.71,
  imageWidth: 1920, imageHeight: 1080,
  focalWideMm: 4.4, focalTeleMm: 132.0,
  cxOffsetMm: 0.0, cyOffsetMm: 0.0,
  zoomMin: 1, zoomMax: 9999,
  panMinDeg: -180, panMaxDeg: 180,
  tiltMinDeg: -20, tiltMaxDeg: 90,
  TMountToPan: ident,
  panAxis: [ 0, 0, -1 ],
  TPanToTilt: ident,
  tiltAxis: [ 0, -1, 0 ],
  TTiltToSensor: ident,
})

// 2. Place the camera in the world.
import { math } from '@leverege/ptz-camera'
const RMount = math.fromEulerXYZDeg([ 0, 0, 90 ])
const camera = new PTZCamera({
  t: [ 0, 0, 10000 ],   // mm
  R: RMount,
  intrinsics,
})

// 3. Use it.
camera.projectPoints([ 5000, 3000, 0 ], Angle.degrees(-30), Angle.degrees(-60), 5000)
camera.xyzToPtd(5000, 3000, 0)
camera.getFov(5000)

Kinematic Chain

A PTZ camera is modeled as a chain of rigid-body transforms from the world frame to the image plane:

world
  |  T_world_to_mount  (extrinsics: position + orientation)
mount
  |  T_mount_to_pan  (usually identity)
pan frame
  |  R(pan_axis, pan)   -- rotation around pan_axis
  |  T_pan_to_tilt      -- offset from pan pivot to tilt pivot
tilt frame
  |  R(tilt_axis, tilt) -- rotation around tilt_axis
  |  T_tilt_to_sensor   -- offset from tilt pivot to sensor
sensor frame  (X=forward, Y=left, Z=up)
  |  R_PROJECTION       -- fixed sensor → projection rotation
projection frame  (X=right, Y=down, Z=forward)

Most cameras leave the inner transforms (T_mount_to_pan, T_pan_to_tilt, T_tilt_to_sensor) at identity. Non-identity values model mechanical offsets (e.g. tilt pivot 50 mm right of the pan axis) that cause parallax visible at close range.

PTZCamera Methods

Pan/tilt are passed as Angle instances (use Angle.degrees(...)). Vectorized methods accept either a single [x, y, z] or an array of points.

getCameraRotation(pan, tilt) -> number[3][3]

Full kinematic-chain rotation. Columns are (right, down, forward) in world coordinates.

const R = camera.getCameraRotation(Angle.degrees(-30), Angle.degrees(-60))

getBasis(pan, tilt) -> [right, down, forward]

The three orthonormal basis vectors as length-3 arrays.

const [ right, down, forward ] = camera.getBasis(Angle.degrees(-30), Angle.degrees(-60))

getDirection(pan, tilt) -> number[3]

Viewing direction unit vector (the forward basis).

const dir = camera.getDirection(Angle.degrees(-30), Angle.degrees(-60))

getOpticalCenter(pan, tilt) -> number[3]

Optical center position in world coordinates. With no kinematic offsets this equals camera.t regardless of pan/tilt.

const center = camera.getOpticalCenter(Angle.degrees(-30), Angle.degrees(-60))

getFov(zoom) -> [hfov, vfov]

Horizontal and vertical FOV at a given zoom, returned as Angle instances.

const [ hfov, vfov ] = camera.getFov(5000)
console.log(`FOV: ${hfov.toDegrees().toFixed(1)} x ${vfov.toDegrees().toFixed(1)}°`)

projectPoints(points, pan, tilt, zoom) -> Array<[u, v, valid]>

Project world points to image pixels. Always returns an array of [u, v, valid] triples (length matches the input). valid is 1 if the point is in front of the camera, 0 otherwise (in which case u and v are zeroed).

// Single point.
const [ [ u, v, valid ] ] = camera.projectPoints(
  [ 5000, 3000, 0 ],
  Angle.degrees(-30), Angle.degrees(-60),
  5000,
)

// Vectorized.
const results = camera.projectPoints(
  [ [ 5000, 3000, 0 ], [ 5500, 3200, 0 ], [ 4800, 2800, 0 ] ],
  Angle.degrees(-30), Angle.degrees(-60),
  5000,
)

isVisible(points, pan, tilt, zoom, marginPx=0) -> boolean[]

Whether each point falls inside the camera's visible frame: in front of the camera and projected pixel coords are within [marginPx, imageSize - marginPx].

const points = [
  [ 5000, 3000, 0 ],     // likely in frame
  [ -9999, 0, 0 ],       // off to the side
]
const visible = camera.isVisible(points, Angle.degrees(-30), Angle.degrees(-60), 5000)
// visible -> [true, false]

// Tighten: require points be at least 50 px from any edge.
const inset = camera.isVisible(points, Angle.degrees(-30), Angle.degrees(-60), 5000, 50)

xyzToPtd(x, y, z) -> [pan, tilt, distance] | null

Find (pan, tilt) that aims the camera at the world point. Returns null if the target coincides with the optical center.

const result = camera.xyzToPtd(5000, 3000, 0)
if (result !== null) {
  const [ pan, tilt, distanceMm ] = result
}

When the camera has translation offsets in the kinematic chain, the optical center moves with pan/tilt; the solver iterates until convergence. Without translation offsets, one iteration is exact.

xyzrToPtz(x, y, z, radius, marginFactor=1.0) -> [pan, tilt, zoom] | null

Find (pan, tilt, zoom) to frame a sphere of given radius (mm) around a world point. Zoom is chosen so the sphere fills the tighter FOV dimension.

// Frame a 200mm-radius object at (5000, 3000, 0) with 1.5x margin.
const result = camera.xyzrToPtz(5000, 3000, 0, 200, 1.5)

xyzToPtz(points, marginFactor=1.0) -> [pan, tilt, zoom] | null

Find (pan, tilt, zoom) to frame all given points within the FOV. Returns null if any point is behind the camera or outside the widest FOV.

const result = camera.xyzToPtz([
  [ 5000, 3000, 0 ],
  [ 5500, 3200, 0 ],
  [ 4800, 2800, 0 ],
], 1.2)

toDict() -> object / PTZCamera.fromDict(data) -> PTZCamera

Serialize a complete camera (mount extrinsics + full intrinsics) to a flat dict, and reconstruct from it. Field naming is snake_case to match the Python py-ptz-camera wire format — dicts produced by either side are interchangeable.

const data = camera.toDict()
// data: { mount_t, mount_r, sensor_*, image_*, focal_*, cx/cy_offset_mm,
//         zoom_min/max, pan/tilt_min/max_deg, pan/tilt_axis,
//         pan_t, pan_r, tilt_t, tilt_r, sensor_t, sensor_r }

const restored = PTZCamera.fromDict(data)

Geometry primitives

Angle

Explicit degree/radian handling.

import { Angle } from '@leverege/ptz-camera'

const a = Angle.degrees(45)
const b = Angle.radians(0.785)

a.toDegrees()   // 45
a.toRadians()   // 0.785...

a.add(b)
a.sub(Angle.degrees(10))
a.mul(2)
a.div(3)
a.normalize([ -180, 180 ])  // wrap to range

Transformation

Rigid body transformation (rotation + translation).

import { Transformation } from '@leverege/ptz-camera'

Transformation.identity()
Transformation.fromTranslation([ 100, 200, 300 ])
Transformation.fromAxisAngle([ 0, 0, 1 ], Angle.degrees(90))
Transformation.fromEulerDeg([ 10, 20, 30 ], [ 100, 200, 300 ])

const T = Transformation.fromEulerDeg([ 10, 20, 30 ])
T.R          // 3x3 row-major rotation matrix
T.t          // length-3 translation vector
T.toEulerDeg()
T.inverse()
T.compose(other)
T.apply(point)        // single [x,y,z] or array of points

Frustum

Camera frustum geometry for visualization.

import { Frustum } from '@leverege/ptz-camera'

const pan = Angle.degrees(-30)
const tilt = Angle.degrees(-60)
const [ hfov, vfov ] = camera.getFov(5000)
const frustum = Frustum.fromCameraBasis({
  position: camera.getOpticalCenter(pan, tilt),
  basis:    camera.getBasis(pan, tilt),
  hfov,
  vfov,
  distance: 10000,   // mm
})
// frustum.position   -- camera optical center
// frustum.direction  -- forward unit vector
// frustum.corners    -- 4 corners at given distance
// frustum.lines      -- line segments for drawing the frustum

LegacyCalibrationParameters

The 5-parameter legacy calibration format (xOffset, yOffset, zOffset, panOffset, tiltOffset) used by older device data. Only valid for inverted-mount configurations (tiltOffset ∈ [175°, 185°]).

import { LegacyCalibrationParameters } from '@leverege/ptz-camera'

const legacy = new LegacyCalibrationParameters({
  xOffset: 10000, yOffset: 0, zOffset: 11000,
  panOffset: 180, tiltOffset: 180,
})
const data = legacy.toDict()
const back = LegacyCalibrationParameters.fromDict(data)

Package Structure

src/
  index.js            -- public API re-exports
  math.js             -- vector/matrix ops, Euler/Rodrigues, nadir flip
  angle.js            -- Angle
  transformation.js   -- Transformation
  intrinsics.js       -- PTZIntrinsics
  camera.js           -- PTZCamera, R_PROJECTION
  frustum.js          -- Frustum
  legacy-calibration.js -- LegacyCalibrationParameters

Relationship to py-ptz-camera

This package mirrors the py-ptz-camera Python library. Wire-format dicts produced by to_dict() (Python) and toDict() (JS) are interchangeable in both directions, so a camera serialized in Python can be reconstructed in JS and vice versa.

What's NOT ported from Python (deliberately scoped out):

  • PTZCalibrationParameters and PTZCalibrationBounds — the calibration-optimizer wrappers.
  • calibrate() — the Differential Evolution + L-BFGS-B + bootstrap pipeline.

Calibration is a Python-only concern. The JS package consumes calibration results, it doesn't produce them.

Development

npm install
npm test            # vitest run
npm run test:watch