Merge pull request 'feature/targetdetection' (#7) from feature/targetdetection into develop

Reviewed-on: #7
This commit is contained in:
2025-11-28 09:21:03 +00:00
13 changed files with 313 additions and 136 deletions

View File

@@ -1,5 +1,5 @@
export const wsConfig = {
infoBar: "ws://100.115.148.59/websocket-infobar",
infoBar: "ws://100.115.125.56/websocket-infobar",
};
export type SocketKey = keyof typeof wsConfig;

View File

@@ -0,0 +1,16 @@
import { createContext, useContext } from "react";
import type { CameraFeedAction, CameraFeedState } from "../../types/types";
type CameraFeedContextType = {
state: CameraFeedState;
// check and refactor
dispatch: (state: CameraFeedAction) => void;
};
export const CameraFeedContext = createContext<CameraFeedContextType | null>(null);
export const useCameraFeedContext = () => {
const ctx = useContext(CameraFeedContext);
if (!ctx) throw new Error("useCameraFeedContext must be used inside <CameraFeedContext.Provider>");
return ctx;
};

View File

@@ -1,11 +1,14 @@
import type { PropsWithChildren } from "react";
import { QueryProvider } from "./QueryProviders";
import { WebSocketProvider } from "./WebSocketProvider";
import { CameraFeedProvider } from "./CameraFeedProvider";
export const AppProviders = ({ children }: PropsWithChildren) => {
return (
<QueryProvider>
<WebSocketProvider>{children}</WebSocketProvider>
<CameraFeedProvider>
<WebSocketProvider>{children}</WebSocketProvider>
</CameraFeedProvider>
</QueryProvider>
);
};

View File

@@ -0,0 +1,9 @@
import { useReducer, type ReactNode } from "react";
import { CameraFeedContext } from "../context/CameraFeedContext";
import { initialState, reducer } from "../reducers/cameraFeedReducer";
export const CameraFeedProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <CameraFeedContext.Provider value={{ state, dispatch }}>{children}</CameraFeedContext.Provider>;
};

View File

@@ -0,0 +1,95 @@
import type { CameraFeedAction, CameraFeedState, PaintedCell } from "../../types/types";
export const initialState: CameraFeedState = {
cameraFeedID: "A",
paintedCells: {
A: new Map<string, PaintedCell>(),
B: new Map<string, PaintedCell>(),
C: new Map<string, PaintedCell>(),
},
regionsByCamera: {
A: [
{ name: "Region 1", brushColour: "#ff0000" },
{ name: "Region 2", brushColour: "#00ff00" },
{ name: "Region 3", brushColour: "#0400ff" },
],
B: [
{ name: "Region 1", brushColour: "#ff0000" },
{ name: "Region 2", brushColour: "#00ff00" },
],
C: [{ name: "Region 1", brushColour: "#ff0000" }],
},
selectedRegionIndex: 0,
modeByCamera: {
A: "brush",
B: "brush",
C: "brush",
},
};
export function reducer(state: CameraFeedState, action: CameraFeedAction) {
switch (action.type) {
case "SET_CAMERA_FEED":
return {
...state,
cameraFeedID: action.payload,
};
case "CHANGE_MODE":
return {
...state,
modeByCamera: {
...state.modeByCamera,
[action.payload.cameraFeedID]: action.payload.mode,
},
};
case "SET_SELECTED_REGION_INDEX":
return {
...state,
selectedRegionIndex: action.payload,
};
case "SET_SELECTED_REGION_COLOUR":
return {
...state,
regionsByCamera: {
...state.regionsByCamera,
[action.payload.cameraFeedID]: state.regionsByCamera[action.payload.cameraFeedID].map((region) =>
region.name === action.payload.regionName ? { ...region, brushColour: action.payload.newColour } : region,
),
},
};
case "ADD_NEW_REGION":
return {
...state,
regionsByCamera: {
...state.regionsByCamera,
[action.payload.cameraFeedID]: [
...state.regionsByCamera[action.payload.cameraFeedID],
{ name: action.payload.regionName, brushColour: action.payload.brushColour },
],
},
};
case "REMOVE_REGION":
console.log(action.payload);
return {
...state,
regionsByCamera: {
...state.regionsByCamera,
[action.payload.cameraFeedID]: state.regionsByCamera[action.payload.cameraFeedID].filter(
(region) => region.name !== action.payload.regionName,
),
},
};
case "RESET_PAINTED_CELLS":
return {
...state,
paintedCells: {
...state.paintedCells,
[state.cameraFeedID]: new Map<string, PaintedCell>(),
},
};
default:
return state;
}
}

View File

@@ -1,52 +1,17 @@
import { useRef, useState } from "react";
import { useState } from "react";
import VideoFeedGridPainter from "./Video/VideoFeedGridPainter";
import CameraSettings from "./CameraSettings/CameraSettings";
import type { PaintedCell, Region } from "../../../types/types";
import PlatePatch from "./PlatePatch/PlatePatch";
const CameraGrid = () => {
const [regions, setRegions] = useState<Region[]>([
{ name: "Region 1", brushColour: "#ff0000" },
{ name: "Region 2", brushColour: "#00ff00" },
{ name: "Region 3", brushColour: "#0400ff" },
]);
const [selectedRegionIndex, setSelectedRegionIndex] = useState(0);
const [mode, setMode] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const updateRegionColour = (index: number, newColour: string) => {
setRegions((prev) => prev.map((r, i) => (i === index ? { ...r, brushColour: newColour } : r)));
};
const paintedCellsRef = useRef<Map<string, PaintedCell>>(new Map());
return (
<div className="grid grid-cols-1 md:grid-cols-5 md:grid-rows-5 max-h-screen">
<VideoFeedGridPainter
regions={regions}
selectedRegionIndex={selectedRegionIndex}
mode={mode}
paintedCells={paintedCellsRef}
/>
<CameraSettings
regions={regions}
selectedRegionIndex={selectedRegionIndex}
onSelectRegion={setSelectedRegionIndex}
onChangeRegionColour={updateRegionColour}
mode={mode}
onSelectMode={setMode}
tabIndex={tabIndex}
setTabIndex={setTabIndex}
paintedCells={paintedCellsRef}
onAddRegion={() => {
setRegions((prev) => [...prev, { name: `Region ${prev.length + 1}`, brushColour: "#ffffff" }]);
}}
OnRemoveRegion={() => {
setRegions((prev) => prev.filter((_, i) => i !== selectedRegionIndex));
setSelectedRegionIndex((prev) => (prev > 0 ? prev - 1 : 0));
}}
/>
<VideoFeedGridPainter />
<CameraSettings tabIndex={tabIndex} setTabIndex={setTabIndex} />
<PlatePatch />
</div>
);

View File

@@ -0,0 +1,61 @@
import { Tabs, Tab, TabList, TabPanel } from "react-tabs";
import { useEffect } from "react";
import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext";
import RegionSelector from "./RegionSelector";
type CameraPanelProps = {
tabIndex: number;
};
const CameraPanel = ({ tabIndex }: CameraPanelProps) => {
const { state, dispatch } = useCameraFeedContext();
const cameraFeedID = state.cameraFeedID;
const regions = state.regionsByCamera[cameraFeedID];
const selectedRegionIndex = state.selectedRegionIndex;
const mode = state.modeByCamera[cameraFeedID];
useEffect(() => {
const mapIndextoCameraId = () => {
switch (tabIndex) {
case 0:
return "A";
case 1:
return "B";
case 2:
return "C";
default:
return "A";
}
};
const cameraId = mapIndextoCameraId();
dispatch({ type: "SET_CAMERA_FEED", payload: cameraId });
}, [dispatch, tabIndex]);
return (
<Tabs>
<TabList>
<Tab>Target Detection</Tab>
<Tab>Camera Controls</Tab>
</TabList>
<TabPanel>
<RegionSelector
regions={regions}
selectedRegionIndex={selectedRegionIndex}
mode={mode}
cameraFeedID={cameraFeedID}
/>
</TabPanel>
<TabPanel>
<div className="p-4">
<h2 className="text-lg font-semibold mb-4">Camera Controls</h2>
<p>Controls for camera {cameraFeedID} will go here.</p>
</div>
</TabPanel>
</Tabs>
);
};
export default CameraPanel;

View File

@@ -1,37 +1,14 @@
import Card from "../../../../ui/Card";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import "react-tabs/style/react-tabs.css";
import RegionSelector from "./RegionSelector";
import type { PaintedCell, Region } from "../../../../types/types";
import type { RefObject } from "react";
import CameraPanel from "./CameraPanel";
type CameraSettingsProps = {
regions: Region[];
selectedRegionIndex: number;
onSelectRegion: (index: number) => void;
onChangeRegionColour: (index: number, colour: string) => void;
mode: string;
onSelectMode: (mode: string) => void;
setTabIndex: (tabIndex: number) => void;
tabIndex: number;
paintedCells: RefObject<Map<string, PaintedCell>>;
onAddRegion: () => void;
OnRemoveRegion: () => void;
};
const CameraSettings = ({
regions,
selectedRegionIndex,
onSelectRegion,
onChangeRegionColour,
mode,
onSelectMode,
tabIndex,
setTabIndex,
paintedCells,
onAddRegion,
OnRemoveRegion,
}: CameraSettingsProps) => {
const CameraSettings = ({ tabIndex, setTabIndex }: CameraSettingsProps) => {
return (
<Card className="p-4 col-span-3 row-span-5 col-start-3 md:col-span-3 md:row-span-5 max-h-screen overflow-auto">
<Tabs
@@ -40,32 +17,18 @@ const CameraSettings = ({
onSelect={(index) => setTabIndex(index)}
>
<TabList>
<Tab>Target Detection</Tab>
<Tab>Camera 1</Tab>
<Tab>Camera 2</Tab>
<Tab>Camera 3</Tab>
<Tab>Camera A</Tab>
<Tab>Camera B</Tab>
<Tab>Camera C</Tab>
</TabList>
<TabPanel>
<RegionSelector
regions={regions}
selectedRegionIndex={selectedRegionIndex}
onSelectRegion={onSelectRegion}
onChangeRegionColour={onChangeRegionColour}
mode={mode}
onSelectMode={onSelectMode}
paintedCells={paintedCells}
onAddRegion={onAddRegion}
OnRemoveRegion={OnRemoveRegion}
/>
<CameraPanel tabIndex={tabIndex} />
</TabPanel>
<TabPanel>
<div>Camera details {tabIndex}</div>
<CameraPanel tabIndex={tabIndex} />
</TabPanel>
<TabPanel>
<div>Camera details {tabIndex}</div>
</TabPanel>
<TabPanel>
<div>Camera details {tabIndex}</div>
<CameraPanel tabIndex={tabIndex} />
</TabPanel>
</Tabs>
</Card>

View File

@@ -1,45 +1,57 @@
import ColourPicker from "./ColourPicker";
import type { PaintedCell, Region } from "../../../../types/types";
import type { RefObject } from "react";
import ColourPicker from "./ColourPicker";
import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext";
type RegionSelectorProps = {
regions: Region[];
selectedRegionIndex: number;
onSelectRegion: (index: number) => void;
onChangeRegionColour: (index: number, colour: string) => void;
mode: string;
onSelectMode: (mode: string) => void;
paintedCells: RefObject<Map<string, PaintedCell>>;
onAddRegion: () => void;
OnRemoveRegion: () => void;
cameraFeedID: "A" | "B" | "C";
};
const RegionSelector = ({
regions,
selectedRegionIndex,
onSelectRegion,
onChangeRegionColour,
mode,
onSelectMode,
paintedCells,
onAddRegion,
OnRemoveRegion,
}: RegionSelectorProps) => {
const RegionSelector = ({ regions, selectedRegionIndex, mode, cameraFeedID }: RegionSelectorProps) => {
const { dispatch } = useCameraFeedContext();
const handleChange = (e: { target: { value: string } }) => {
onSelectMode(e.target.value);
dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: e.target.value } });
};
const handleAddClick = () => {
onAddRegion();
const handleAddRegionClick = () => {
const regionName = `Region ${regions.length + 1}`;
dispatch({
type: "ADD_NEW_REGION",
payload: { cameraFeedID: cameraFeedID, regionName: regionName, brushColour: "#ffffff" },
});
};
const handleResetClick = () => {
const map = paintedCells.current;
map.clear();
const handleResetRegion = () => {
dispatch({
type: "RESET_PAINTED_CELLS",
payload: { cameraFeedID: cameraFeedID, paintedCells: new Map<string, PaintedCell>() },
});
};
const handleRemoveClick = () => {
OnRemoveRegion();
dispatch({
type: "REMOVE_REGION",
payload: { cameraFeedID: cameraFeedID, regionName: regions[selectedRegionIndex].name },
});
};
const handleModeChange = (newMode: string) => {
dispatch({ type: "CHANGE_MODE", payload: { cameraFeedID: cameraFeedID, mode: newMode } });
};
const handleRegionSelect = (index: number) => {
dispatch({ type: "SET_SELECTED_REGION_INDEX", payload: index });
};
const handleRegionColourChange = (index: number, newColour: string) => {
const regionName = regions[index].name;
dispatch({
type: "SET_SELECTED_REGION_COLOUR",
payload: { cameraFeedID: cameraFeedID, regionName: regionName, newColour: newColour },
});
};
return (
@@ -84,7 +96,7 @@ const RegionSelector = ({
<div className="p-2 border border-gray-600 rounded-lg flex flex-col">
<h2 className="text-2xl mb-2">Region Select</h2>
<>
{regions.map((region, idx) => {
{regions?.map((region, idx) => {
const isSelected = selectedRegionIndex === idx;
const inputId = `region-${idx}`;
return (
@@ -102,20 +114,20 @@ const RegionSelector = ({
name="region"
className="sr-only"
onChange={() => {
onSelectMode("painter");
onSelectRegion(idx);
handleModeChange("painter");
handleRegionSelect(idx);
}}
/>
<span className="text-xl">{region.name}</span>
</div>
<ColourPicker colour={region.brushColour} setColour={(c: string) => onChangeRegionColour(idx, c)} />
<ColourPicker colour={region.brushColour} setColour={(c: string) => handleRegionColourChange(idx, c)} />
<p className="text-slate-400">{region.brushColour}</p>
</label>
);
})}
</>
<div className=" mx-auto flex flex-row gap-4 mt-4">
<button className="border border-blue-900 bg-blue-700 px-4 rounded-md" onClick={handleAddClick}>
<button className="border border-blue-900 bg-blue-700 px-4 rounded-md" onClick={handleAddRegionClick}>
Add Region
</button>
<button className="border border-red-900 px-4 rounded-md" onClick={handleRemoveClick}>
@@ -128,10 +140,10 @@ const RegionSelector = ({
<div className="flex flex-col">
<h2 className="text-2xl mb-2">Actions</h2>
<button
onClick={handleResetClick}
onClick={handleResetRegion}
className="mt-2 px-4 py-2 border border-red-600 rounded-md text-white bg-red-600 w-full md:w-[40%] hover:bg-red-700 hover:cursor-pointer"
>
Reset Regions
Reset Region
</button>
</div>
</div>

View File

@@ -2,22 +2,22 @@ import { useEffect, useRef, useState, type RefObject } from "react";
import { Stage, Layer, Image, Shape } from "react-konva";
import type { KonvaEventObject } from "konva/lib/Node";
import { useCreateVideoSnapshot } from "../../hooks/useGetvideoSnapshots";
import type { PaintedCell, Region } from "../../../../types/types";
import Card from "../../../../ui/Card";
import { useCameraFeedContext } from "../../../../app/context/CameraFeedContext";
const rows = 40;
const cols = 40;
const size = 20;
const gap = 0;
type VideoFeedGridPainterProps = {
regions: Region[];
selectedRegionIndex: number;
mode: string;
paintedCells: RefObject<Map<string, PaintedCell>>;
};
const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode, paintedCells }: VideoFeedGridPainterProps) => {
const VideoFeedGridPainter = () => {
const { state } = useCameraFeedContext();
const cameraFeedID = state.cameraFeedID;
const paintedCells = state.paintedCells[cameraFeedID];
const regions = state.regionsByCamera[cameraFeedID];
const selectedRegionIndex = state.selectedRegionIndex;
const mode = state.modeByCamera[cameraFeedID];
const { latestBitmapRef, isloading } = useCreateVideoSnapshot();
const [stageSize, setStageSize] = useState({ width: 740, height: 460 });
const isDrawingRef = useRef(false);
@@ -47,7 +47,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode, paintedCells
const key = `${row}-${col}`;
const currentColour = regions[selectedRegionIndex].brushColour;
const map = paintedCells.current;
const map = paintedCells;
const existing = map.get(key);
if (mode === "eraser") {
@@ -121,7 +121,7 @@ const VideoFeedGridPainter = ({ regions, selectedRegionIndex, mode, paintedCells
<Layer ref={paintLayerRef} opacity={0.6}>
<Shape
sceneFunc={(ctx, shape) => {
const cells = paintedCells.current;
const cells = paintedCells;
cells.forEach((cell, key) => {
const [rowStr, colStr] = key.split("-");
const row = Number(rowStr);

View File

@@ -1,7 +1,8 @@
import { useQuery } from "@tanstack/react-query";
import { CAMBASE } from "../../../utils/config";
const getfeed = async () => {
const response = await fetch(`http://100.115.148.59/TargetDetectionColour-preview`, {
const getfeed = async (cameraFeedID: "A" | "B" | "C" | null) => {
const response = await fetch(`${CAMBASE}TargetDetectionColour${cameraFeedID}-preview`, {
signal: AbortSignal.timeout(300000),
cache: "no-store",
});
@@ -11,10 +12,10 @@ const getfeed = async () => {
return response.blob();
};
export const useGetVideoFeed = () => {
export const useGetVideoFeed = (cameraFeedID: "A" | "B" | "C" | null) => {
const videoQuery = useQuery({
queryKey: ["getfeed"],
queryFn: getfeed,
queryKey: ["getfeed", cameraFeedID],
queryFn: () => getfeed(cameraFeedID),
refetchInterval: 500,
});

View File

@@ -1,9 +1,12 @@
import { useEffect, useRef } from "react";
import { useGetVideoFeed } from "./useGetVideoFeed";
import { useCameraFeedContext } from "../../../app/context/CameraFeedContext";
export const useCreateVideoSnapshot = () => {
const { state } = useCameraFeedContext();
const cameraFeedID = state?.cameraFeedID;
const latestBitmapRef = useRef<ImageBitmap | null>(null);
const { videoQuery } = useGetVideoFeed();
const { videoQuery } = useGetVideoFeed(cameraFeedID);
const snapShot = videoQuery?.data;
const isloading = videoQuery.isPending;

View File

@@ -95,3 +95,52 @@ export type OptionalBOF2LaneIDs = {
LID2?: string;
LID3?: string;
};
export type CameraFeedState = {
cameraFeedID: "A" | "B" | "C";
paintedCells: {
A: Map<string, PaintedCell>;
B: Map<string, PaintedCell>;
C: Map<string, PaintedCell>;
};
regionsByCamera: {
A: Region[];
B: Region[];
C: Region[];
};
selectedRegionIndex: number;
modeByCamera: {
A: string;
B: string;
C: string;
};
tabIndex?: number;
};
export type CameraFeedAction =
| {
type: "SET_CAMERA_FEED";
payload: "A" | "B" | "C";
}
| {
type: "CHANGE_MODE";
payload: { cameraFeedID: "A" | "B" | "C"; mode: string };
}
| { type: "SET_SELECTED_REGION_INDEX"; payload: number }
| {
type: "SET_SELECTED_REGION_COLOUR";
payload: { cameraFeedID: "A" | "B" | "C"; regionName: string; newColour: string };
}
| {
type: "ADD_NEW_REGION";
payload: { cameraFeedID: "A" | "B" | "C"; regionName: string; brushColour: string };
}
| {
type: "REMOVE_REGION";
payload: { cameraFeedID: "A" | "B" | "C"; regionName: string };
}
| {
type: "RESET_PAINTED_CELLS";
payload: { cameraFeedID: "A" | "B" | "C"; paintedCells: Map<string, PaintedCell> };
};