@blueharford/scrypted-spatial-awareness 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueharford/scrypted-spatial-awareness",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Cross-camera object tracking for Scrypted NVR with spatial awareness",
5
5
  "author": "Joshua Seidel <blueharford>",
6
6
  "license": "Apache-2.0",
@@ -659,7 +659,9 @@ export class TopologyDiscoveryEngine {
659
659
  type: landmark.type,
660
660
  description: landmark.description,
661
661
  visibleFromCameras: [analysis.cameraId],
662
- },
662
+ // Include bounding box for positioning (will be used by applyDiscoverySuggestion)
663
+ boundingBox: landmark.boundingBox,
664
+ } as any, // boundingBox is extra metadata not in Landmark interface
663
665
  };
664
666
  this.suggestions.set(suggestion.id, suggestion);
665
667
  }
package/src/main.ts CHANGED
@@ -1861,21 +1861,66 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1861
1861
  const range = fov.range || 80;
1862
1862
  const fovAngle = fov.angle || 90;
1863
1863
 
1864
- // Count existing landmarks from this camera to spread them out
1865
- const cameraDeviceId = camera.deviceId;
1866
- const existingFromCamera = (topology.landmarks || []).filter(l =>
1867
- l.visibleFromCameras?.includes(cameraDeviceId) ||
1868
- l.visibleFromCameras?.includes(camera.name)
1869
- ).length;
1870
-
1871
1864
  // Calculate position in front of camera within its FOV
1872
1865
  // Convert direction to radians (0 = up/north, 90 = right/east)
1873
1866
  const dirRad = (direction - 90) * Math.PI / 180;
1874
1867
  const halfFov = (fovAngle / 2) * Math.PI / 180;
1875
1868
 
1876
- // Spread landmarks across the FOV cone at varying distances
1877
- const angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6; // -0.6, 0, +0.6 of half FOV
1878
- const distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3; // 50% or 80% of range
1869
+ // Use landmark TYPE to determine distance - some things are inherently far away
1870
+ // regardless of where they appear in the camera frame
1871
+ const landmarkType = suggestion.landmark.type;
1872
+ const farTypes = ['neighbor', 'boundary', 'street']; // Always place at edge of FOV
1873
+ const isFarType = farTypes.includes(landmarkType || '');
1874
+
1875
+ // Use bounding box if available for horizontal positioning
1876
+ // boundingBox format: [x, y, width, height] normalized 0-1
1877
+ const bbox = (suggestion.landmark as any).boundingBox as [number, number, number, number] | undefined;
1878
+
1879
+ let angleOffset: number;
1880
+ let distanceMultiplier: number;
1881
+
1882
+ if (bbox && bbox.length >= 2) {
1883
+ // Use bounding box X for horizontal position in FOV
1884
+ const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
1885
+
1886
+ // Map X position to angle within FOV
1887
+ angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
1888
+
1889
+ // For distance: use TYPE first, then bbox Y as hint
1890
+ if (isFarType) {
1891
+ // Neighbors, boundaries, streets are BEYOND the camera's normal range
1892
+ // Place at 150-200% of range to indicate they're distant background features
1893
+ distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
1894
+ this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
1895
+ } else {
1896
+ // For other types, use bbox Y as a hint (but cap minimum distance)
1897
+ const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
1898
+ // Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
1899
+ distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
1900
+ }
1901
+
1902
+ this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
1903
+ } else {
1904
+ // No bounding box - use type and spread pattern
1905
+ const cameraDeviceId = camera.deviceId;
1906
+ const existingFromCamera = (topology.landmarks || []).filter(l =>
1907
+ l.visibleFromCameras?.includes(cameraDeviceId) ||
1908
+ l.visibleFromCameras?.includes(camera.name)
1909
+ ).length;
1910
+
1911
+ // Spread horizontally across FOV
1912
+ angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
1913
+
1914
+ // Distance based on type
1915
+ if (isFarType) {
1916
+ // Neighbors, boundaries, streets are BEYOND the camera's normal range
1917
+ distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
1918
+ this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
1919
+ } else {
1920
+ distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
1921
+ this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
1922
+ }
1923
+ }
1879
1924
 
1880
1925
  const finalAngle = dirRad + angleOffset;
1881
1926
  const distance = range * distanceMultiplier;
@@ -1885,7 +1930,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1885
1930
  y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distance,
1886
1931
  };
1887
1932
 
1888
- this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" in ${camera.name}'s FOV: dir=${direction}°, dist=${distance.toFixed(0)}px`);
1933
+ this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" in ${camera.name}'s FOV: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°, dist=${distance.toFixed(0)}px`);
1889
1934
  } else {
1890
1935
  // Position in a grid pattern starting from center
1891
1936
  const landmarkCount = topology.landmarks?.length || 0;
@@ -1956,35 +2001,65 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1956
2001
  const dirRad = (direction - 90) * Math.PI / 180;
1957
2002
  const halfFov = (fovAngle / 2) * Math.PI / 180;
1958
2003
 
1959
- // Count existing zones from this camera to offset new ones
1960
- const existingFromCamera = (topology.drawnZones || []).filter((z: any) =>
1961
- z.linkedCameras?.includes(sourceCameras[0])
1962
- ).length;
2004
+ const camX = camera.floorPlanPosition.x;
2005
+ const camY = camera.floorPlanPosition.y;
1963
2006
 
1964
- // Create a wedge-shaped zone within the camera's FOV
1965
- // Offset based on existing zones to avoid overlap
1966
- const innerRadius = range * 0.3 + existingFromCamera * 20;
1967
- const outerRadius = range * 0.8 + existingFromCamera * 20;
2007
+ // Use bounding box if available to position zone accurately within FOV
2008
+ const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
1968
2009
 
1969
- // Use a portion of the FOV for each zone
1970
- const zoneSpread = halfFov * 0.7; // 70% of half FOV
2010
+ let innerRadius: number;
2011
+ let outerRadius: number;
2012
+ let angleStart: number;
2013
+ let angleEnd: number;
1971
2014
 
1972
- const camX = camera.floorPlanPosition.x;
1973
- const camY = camera.floorPlanPosition.y;
2015
+ if (bbox && bbox.length >= 4) {
2016
+ // Map bounding box to position within FOV
2017
+ const bboxLeft = bbox[0];
2018
+ const bboxRight = bbox[0] + bbox[2];
2019
+ const bboxTop = bbox[1];
2020
+ const bboxBottom = bbox[1] + bbox[3];
2021
+
2022
+ // Map X to angle within FOV (0 = left edge, 1 = right edge)
2023
+ angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
2024
+ angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
2025
+
2026
+ // Map Y to distance (0 = far, 1 = close)
2027
+ innerRadius = range * (0.9 - bboxBottom * 0.6);
2028
+ outerRadius = range * (0.9 - bboxTop * 0.6);
2029
+
2030
+ // Ensure min size
2031
+ if (outerRadius - innerRadius < 20) {
2032
+ outerRadius = innerRadius + 20;
2033
+ }
2034
+
2035
+ this.console.log(`[Discovery] Zone "${zone.name}" using bbox [${bbox.join(',')}] → angles ${(angleStart * 180 / Math.PI).toFixed(1)}° to ${(angleEnd * 180 / Math.PI).toFixed(1)}°`);
2036
+ } else {
2037
+ // Fallback: wedge-shaped zone offset by existing count
2038
+ const existingFromCamera = (topology.drawnZones || []).filter((z: any) =>
2039
+ z.linkedCameras?.includes(sourceCameras[0])
2040
+ ).length;
2041
+
2042
+ innerRadius = range * 0.3 + existingFromCamera * 20;
2043
+ outerRadius = range * 0.8 + existingFromCamera * 20;
2044
+ angleStart = dirRad - halfFov * 0.7;
2045
+ angleEnd = dirRad + halfFov * 0.7;
2046
+
2047
+ this.console.log(`[Discovery] Zone "${zone.name}" using fallback spread (existing: ${existingFromCamera})`);
2048
+ }
1974
2049
 
1975
- // Create arc polygon (wedge shape)
2050
+ // Create arc polygon
1976
2051
  const steps = 8;
1977
- // Inner arc (from left to right)
2052
+ // Inner arc (from start angle to end angle)
1978
2053
  for (let i = 0; i <= steps; i++) {
1979
- const angle = dirRad - zoneSpread + (zoneSpread * 2 * i / steps);
2054
+ const angle = angleStart + (angleEnd - angleStart) * i / steps;
1980
2055
  polygon.push({
1981
2056
  x: camX + Math.cos(angle) * innerRadius,
1982
2057
  y: camY + Math.sin(angle) * innerRadius,
1983
2058
  });
1984
2059
  }
1985
- // Outer arc (from right to left)
2060
+ // Outer arc (from end angle to start angle)
1986
2061
  for (let i = steps; i >= 0; i--) {
1987
- const angle = dirRad - zoneSpread + (zoneSpread * 2 * i / steps);
2062
+ const angle = angleStart + (angleEnd - angleStart) * i / steps;
1988
2063
  polygon.push({
1989
2064
  x: camX + Math.cos(angle) * outerRadius,
1990
2065
  y: camY + Math.sin(angle) * outerRadius,