From 155522182545988e58f74f6422ff2ed9279b3af1 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 9 Jan 2026 16:16:28 +0000 Subject: [PATCH] - added region painting context and components - can switch to target detection on region select --- src/app/reducers/cameraSettingsReducer.ts | 32 ++++- .../cameraControls/CameraControls.tsx | 7 +- .../components/cameraSetup/CameraSetup.tsx | 18 ++- .../components/platePatch/PlatePatchSetup.tsx | 4 +- .../setup/components/region/Region.tsx | 118 ++++++++++++++++++ .../components/videofeed/VideoFeedSetup.tsx | 91 +++++++++++++- .../setup/hooks/useCreatePreviewImage.ts | 15 ++- src/features/setup/hooks/useVideoPreview.ts | 20 ++- src/utils/types.ts | 28 ++++- 9 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 src/features/setup/components/region/Region.tsx diff --git a/src/app/reducers/cameraSettingsReducer.ts b/src/app/reducers/cameraSettingsReducer.ts index d5307ec..d62654d 100644 --- a/src/app/reducers/cameraSettingsReducer.ts +++ b/src/app/reducers/cameraSettingsReducer.ts @@ -1,19 +1,28 @@ import type { CameraSettings, CameraSettingsAction } from "../../utils/types"; export const initialState: CameraSettings = { + cameraMode: 0, mode: 0, imageSize: { width: 1280, height: 960 }, regionPainter: { - paintedCells: [], + paintmode: "painter", + paintedCells: new Map(), regions: [ - { name: "Region 1", brushColour: "#FF0000", cells: [] }, - { name: "Region 2", brushColour: "#00FF00", cells: [] }, + { name: "Region 1", brushColour: "#FF0000" }, + { name: "Region 2", brushColour: "#00FF00" }, + { name: "Region 3", brushColour: "#0000FF" }, ], + selectedRegionIndex: 0, }, }; export const cameraSettingsReducer = (state: CameraSettings, action: CameraSettingsAction) => { switch (action.type) { + case "SET_CAMERA_MODE": + return { + ...state, + cameraMode: action.payload, + }; case "SET_MODE": return { ...state, @@ -24,6 +33,23 @@ export const cameraSettingsReducer = (state: CameraSettings, action: CameraSetti ...state, imageSize: action.payload, }; + case "SET_REGION_PAINTMODE": + return { + ...state, + regionPainter: { + ...state.regionPainter, + paintmode: action.payload, + }, + }; + + case "SET_SELECTED_REGION_INDEX": + return { + ...state, + regionPainter: { + ...state.regionPainter, + selectedRegionIndex: action.payload, + }, + }; default: return state; } diff --git a/src/features/setup/components/cameraControls/CameraControls.tsx b/src/features/setup/components/cameraControls/CameraControls.tsx index 97e30c5..24dcc53 100644 --- a/src/features/setup/components/cameraControls/CameraControls.tsx +++ b/src/features/setup/components/cameraControls/CameraControls.tsx @@ -1,13 +1,14 @@ import { Formik, Form } from "formik"; +import type { CameraSettings } from "../../../../utils/types"; type CameraControlProps = { - tabIndex: number; + state: CameraSettings; }; -const CameraControls = ({ tabIndex }: CameraControlProps) => { +const CameraControls = ({ state }: CameraControlProps) => { + console.log(state); const initialValues = {}; - console.log(tabIndex); const handleSumbit = (values: { test?: string }) => { console.log(values); }; diff --git a/src/features/setup/components/cameraSetup/CameraSetup.tsx b/src/features/setup/components/cameraSetup/CameraSetup.tsx index ecff462..a82291d 100644 --- a/src/features/setup/components/cameraSetup/CameraSetup.tsx +++ b/src/features/setup/components/cameraSetup/CameraSetup.tsx @@ -3,24 +3,32 @@ import "react-tabs/style/react-tabs.css"; import Card from "../../../../components/ui/Card"; import CameraControls from "../cameraControls/CameraControls"; import { useState } from "react"; +import Region from "../region/Region"; +import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext"; const CameraSetup = () => { const [tabIndex, setTabIndex] = useState(0); - console.log(tabIndex); + const { state, dispatch } = useCameraSettingsContext(); return ( - setTabIndex(index)}> + { + dispatch({ type: "SET_CAMERA_MODE", payload: index }); + setTabIndex(index); + }} + > Camera - Regions + Target Detection Crop Advanced - + -
Regions
+
Crop
diff --git a/src/features/setup/components/platePatch/PlatePatchSetup.tsx b/src/features/setup/components/platePatch/PlatePatchSetup.tsx index 48b765d..9076b16 100644 --- a/src/features/setup/components/platePatch/PlatePatchSetup.tsx +++ b/src/features/setup/components/platePatch/PlatePatchSetup.tsx @@ -21,8 +21,8 @@ const PlatePatchSetup = () => { - {sightingList?.map((sighting) => ( - + {sightingList?.map((sighting, index) => ( + {sighting.vrm} {sighting.seenCount} {sighting.timeStamp} diff --git a/src/features/setup/components/region/Region.tsx b/src/features/setup/components/region/Region.tsx new file mode 100644 index 0000000..e2bfb23 --- /dev/null +++ b/src/features/setup/components/region/Region.tsx @@ -0,0 +1,118 @@ +import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext"; +import type { CameraSettings } from "../../../../utils/types"; + +type RegionProps = { + state: CameraSettings; +}; + +const Region = ({ state }: RegionProps) => { + const { dispatch } = useCameraSettingsContext(); + const paintMode = state.regionPainter.paintmode; + const regions = state.regionPainter.regions; + + const handleChangePaintMode = (event: React.ChangeEvent) => { + const mode = event.target.value as "painter" | "eraser"; + dispatch({ type: "SET_REGION_PAINTMODE", payload: mode }); + }; + + const handlePaintMode = (mode: "painter" | "eraser") => dispatch({ type: "SET_REGION_PAINTMODE", payload: mode }); + + const handleChangeRegion = (idx: number) => { + dispatch({ type: "SET_SELECTED_REGION_INDEX", payload: idx }); + }; + + const handleSaveClick = () => { + console.log(state); + }; + return ( +
+
+
+

Tools

+
+ + +
+
+
+

Regions (Lanes)

+
+ {regions.map((region, idx) => { + const inputID = `region-${idx}`; + return ( + + ); + })} +
+
+
+
+

Actions

+
+ + +
+
+
+ ); +}; + +export default Region; diff --git a/src/features/setup/components/videofeed/VideoFeedSetup.tsx b/src/features/setup/components/videofeed/VideoFeedSetup.tsx index 54ff8db..f0c6ec7 100644 --- a/src/features/setup/components/videofeed/VideoFeedSetup.tsx +++ b/src/features/setup/components/videofeed/VideoFeedSetup.tsx @@ -1,12 +1,31 @@ -import { Stage, Layer, Image } from "react-konva"; +import { Stage, Layer, Image, Shape } from "react-konva"; import { useCreateVideoPreviewSnapshot } from "../../hooks/useCreatePreviewImage"; -import { useEffect, type RefObject } from "react"; +import { useEffect, useRef, type RefObject } from "react"; import { useCameraSettingsContext } from "../../../../app/context/CameraSettingsContext"; +import type { KonvaEventObject } from "konva/lib/Node"; + +const BACKEND_WIDTH = 640; + +const BACKEND_CELL_SIZE = 16; + +const rows = 22.5; +const cols = 40; +const gap = 0; const VideoFeedSetup = () => { const { latestBitmapRef, isLoading } = useCreateVideoPreviewSnapshot(); const { state, dispatch } = useCameraSettingsContext(); + const cameraMode = state.cameraMode; + const paintedCells = state.regionPainter.paintedCells; + const paintMode = state.regionPainter.paintmode; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const paintLayerRef = useRef(null); const size = state.imageSize; + const selectedRegionIndex = state.regionPainter.selectedRegionIndex; + const region = state.regionPainter.regions[selectedRegionIndex]; + + const currentScale = size.width / BACKEND_WIDTH; + const cellSize = BACKEND_CELL_SIZE * currentScale; const draw = (bmp: RefObject): ImageBitmap | null => { if (!bmp || !bmp.current) { @@ -17,6 +36,45 @@ const VideoFeedSetup = () => { }; const image = draw(latestBitmapRef); + const paintCell = (x: number, y: number) => { + const col = Math.floor(x / (cellSize + gap)); + const row = Math.floor(y / (cellSize + gap)); + + if (row < 0 || row >= rows || col < 0 || col >= cols) return; + + const activeRegion = region; + if (!activeRegion) return; + const cellKey = `${row}-${col}`; + const currentColour = region.brushColour; + + const map = paintedCells; + const existing = map.get(cellKey); + + if (paintMode === "eraser") { + if (map.has(cellKey)) { + map.delete(cellKey); + paintLayerRef.current?.batchDraw(); + } + return; + } + + if (existing && existing.colour === currentColour) return; + map.set(cellKey, { colour: currentColour, region: activeRegion }); + paintLayerRef.current?.batchDraw(); + }; + + const handleStageMouseDown = (e: KonvaEventObject) => { + if (!(cameraMode === 1)) return; + const pos = e.target.getStage()?.getPointerPosition(); + if (pos) paintCell(pos.x, pos.y); + }; + + const handleMouseMove = (e: KonvaEventObject) => { + if (!(cameraMode === 1)) return; + const pos = e.target.getStage()?.getPointerPosition(); + if (pos && e.evt.buttons === 1) paintCell(pos.x, pos.y); + }; + useEffect(() => { const updateSize = () => { const width = window.innerWidth * 0.48; @@ -31,8 +89,35 @@ const VideoFeedSetup = () => { if (isLoading) return <>Loading...; return (
- + {image && } + + {cameraMode === 1 && ( + { + const cells = paintedCells; + if (!cells || cells.size === 0 || !paintLayerRef.current) return; + cells?.forEach((cell, key) => { + const [rowStr, colStr] = key.split("-"); + const row = Number(rowStr); + const col = Number(colStr); + + const x = col * (cellSize + gap); + const y = row * (cellSize + gap); + + ctx.beginPath(); + ctx.rect(x, y, cellSize, cellSize); + ctx.fillStyle = cell.colour; + ctx.fill(); + }); + + ctx.fillStrokeShape(shape); + }} + width={size.width} + height={size.height} + /> + )} +
); diff --git a/src/features/setup/hooks/useCreatePreviewImage.ts b/src/features/setup/hooks/useCreatePreviewImage.ts index b8bb928..1f753fe 100644 --- a/src/features/setup/hooks/useCreatePreviewImage.ts +++ b/src/features/setup/hooks/useCreatePreviewImage.ts @@ -1,12 +1,19 @@ import { useEffect, useRef } from "react"; import { useVideoPreview } from "./useVideoPreview"; +import { useCameraSettingsContext } from "../../../app/context/CameraSettingsContext"; export const useCreateVideoPreviewSnapshot = () => { - const { videoPreviewQuery } = useVideoPreview(); + const { state } = useCameraSettingsContext(); + const { videoPreviewQuery, targetDetectionFeedQuery } = useVideoPreview(state.cameraMode); const latestBitmapRef = useRef(null); - const isLoading = videoPreviewQuery?.isPending; - const imageBlob = videoPreviewQuery?.data; - + const isLoading = videoPreviewQuery?.isPending || targetDetectionFeedQuery?.isPending; + let snapshot; + if (state.cameraMode === 0) { + snapshot = videoPreviewQuery?.data; + } else if (state.cameraMode === 1) { + snapshot = targetDetectionFeedQuery?.data; + } + const imageBlob = snapshot; useEffect(() => { async function createImageBitmapFromBlob() { if (!imageBlob) return; diff --git a/src/features/setup/hooks/useVideoPreview.ts b/src/features/setup/hooks/useVideoPreview.ts index bb2e433..e4698d1 100644 --- a/src/features/setup/hooks/useVideoPreview.ts +++ b/src/features/setup/hooks/useVideoPreview.ts @@ -9,11 +9,27 @@ const fetchVideoPreview = async () => { return response.blob(); }; -export const useVideoPreview = () => { +const fetchTargetDectionFeed = async () => { + const response = await fetch(`${cambase}/TargetDetectionColour-preview`); + if (!response.ok) { + throw new Error("Failed to fetch target detection feed"); + } + return response.blob(); +}; + +export const useVideoPreview = (mode: number) => { const videoPreviewQuery = useQuery({ queryKey: ["videoPreview"], queryFn: fetchVideoPreview, refetchInterval: 100, + enabled: mode === 0, }); - return { videoPreviewQuery }; + + const targetDetectionFeedQuery = useQuery({ + queryKey: ["targetDetectionFeed"], + queryFn: fetchTargetDectionFeed, + refetchInterval: 100, + enabled: mode === 1, + }); + return { videoPreviewQuery, targetDetectionFeedQuery }; }; diff --git a/src/utils/types.ts b/src/utils/types.ts index a8177b2..cd170a4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -52,12 +52,25 @@ export type NpedJSON = { "INSURANCE STATUS": string; }; +export type Region = { + name: string; + brushColour: string; +}; + +export type PaintedCell = { + colour: string; + region: Region; +}; + export type CameraSettings = { + cameraMode: number; mode: number; imageSize: { width: number; height: number }; regionPainter: { - paintedCells: { x: number; y: number }[]; - regions: { name: string; brushColour: string; cells: { x: number; y: number }[] }[]; + paintmode: "painter" | "eraser"; + paintedCells: Map; + regions: Region[]; + selectedRegionIndex: number; }; }; export type CameraSettingsAction = @@ -68,7 +81,16 @@ export type CameraSettingsAction = | { type: "SET_IMAGE_SIZE"; payload: { width: number; height: number }; - }; + } + | { + type: "SET_CAMERA_MODE"; + payload: number; + } + | { + type: "SET_REGION_PAINTMODE"; + payload: "painter" | "eraser"; + } + | { type: "SET_SELECTED_REGION_INDEX"; payload: number }; export type CameraStatus = { id: string;