Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ def update_labels(label_names: list[str], offset: int = 0):

visualizer.addTopic("Detections", annotation_node.out)

def get_current_params_service() -> dict[str, any]:
"""Returns current parameters used"""
return {
"confidence_threshold": CONFIDENCE_THRESHOLD,
"class_names": CLASS_NAMES,
}

def class_update_service(new_classes: list[str]):
"""Changes classes to detect based on the user input"""
if len(new_classes) == 0:
Expand Down Expand Up @@ -256,7 +263,7 @@ def class_update_service(new_classes: list[str]):

def conf_threshold_update_service(new_conf_threshold: float):
"""Changes confidence threshold based on the user input"""
CONFIDENCE_THRESHOLD = max(0, min(1, new_conf_threshold))
CONFIDENCE_THRESHOLD = max(0.01, min(0.99, new_conf_threshold))
nn_with_parser.getParser(0).setConfidenceThreshold(CONFIDENCE_THRESHOLD)
print(f"Confidence threshold set to: {CONFIDENCE_THRESHOLD}:")

Expand Down Expand Up @@ -673,6 +680,7 @@ def bbox_prompt_service(payload):
)
return {"ok": True, "bbox": {"x0": x0, "y0": y0, "x1": x1, "y1": y1}}

visualizer.registerService("Get Current Params Service", get_current_params_service)
visualizer.registerService("Class Update Service", class_update_service)
visualizer.registerService(
"Threshold Update Service", conf_threshold_update_service
Expand Down
257 changes: 163 additions & 94 deletions custom-frontend/open-vocabulary-object-detection/frontend/src/App.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,71 +1,102 @@
import { Flex, Button, Input } from "@luxonis/common-fe-components";
import { useRef, useState } from "react";
import { css } from "../styled-system/css/css.mjs";
import { useRef, useState } from "react";
import { useConnection } from "@luxonis/depthai-viewer-common";
import { useNotifications } from "./Notifications.tsx";

type Props = {
onClassesUpdated?: (classes: string[]) => void;
interface ClassSelectorProps {
initialClasses?: string[];
onClassesUpdated?: (classes: string[]) => void;
}

export function ClassSelector({ onClassesUpdated }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const connection = useConnection();
const [selectedClasses, setSelectedClasses] = useState<string[]>(["person", "chair", "TV"]);
const { notify } = useNotifications();

const handleSendMessage = () => {
if (inputRef.current) {
const value = inputRef.current.value;
const updatedClasses = value
.split(',')
.map((c: string) => c.trim())
.filter(Boolean);
export function ClassSelector({ initialClasses = [], onClassesUpdated }: ClassSelectorProps) {
const inputRef = useRef<HTMLInputElement>(null);
const connection = useConnection();
const [selectedClasses, setSelectedClasses] = useState<string[]>(initialClasses);
const { notify } = useNotifications();

if (updatedClasses.length === 0) {
notify('Please enter at least one class (comma separated).', { type: 'warning', durationMs: 5000 });
return;
}
if (!connection.connected) {
notify('Not connected to device. Unable to update classes.', { type: 'error' });
return;
}
const handleSendMessage = () => {
if (inputRef.current) {
const value = inputRef.current.value;
const updatedClasses = value
.split(",")
.map((c: string) => c.trim())
.filter(Boolean);

console.log('Sending new class list to backend:', updatedClasses);
notify(`Updating ${updatedClasses.length} class${updatedClasses.length > 1 ? 'es' : ''}…`, { type: 'info' });
if (updatedClasses.length === 0) {
notify("Please enter at least one class (comma separated).", {
type: "warning",
durationMs: 5000,
});
return;
}
if (!connection.connected) {
notify("Not connected to device. Unable to update classes.", {
type: "error",
});
return;
}

connection.daiConnection?.postToService(
// @ts-ignore - Custom service
"Class Update Service",
updatedClasses,
() => {
console.log('Backend acknowledged class update');
setSelectedClasses(updatedClasses);
notify(`Classes updated (${updatedClasses.join(', ')})`, { type: 'success', durationMs: 6000 });
onClassesUpdated?.(updatedClasses);
}
);
console.log("Sending new class list to backend:", updatedClasses);
notify(
`Updating ${updatedClasses.length} class${
updatedClasses.length > 1 ? "es" : ""
}…`,
{ type: "info" }
);

inputRef.current.value = '';
connection.daiConnection?.postToService(
// @ts-ignore - Custom service
"Class Update Service",
updatedClasses,
() => {
console.log("Backend acknowledged class update");
setSelectedClasses(updatedClasses);
notify(`Classes updated (${updatedClasses.join(", ")})`, {
type: "success",
durationMs: 6000,
});
onClassesUpdated?.(updatedClasses);
}
};
);

return (
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'sm' })}>
{/* Class List Display */}
<h3 className={css({ fontWeight: "semibold" })}>Update Classes with Text Input:</h3>
<ul className={css({ listStyleType: 'disc', paddingLeft: 'lg' })}>
{selectedClasses.map((cls: string, idx: number) => (
<li key={idx}>{cls}</li>
))}
</ul>
inputRef.current.value = "";
}
};


{/* Input + Button */}
<Flex direction="row" gap="sm" alignItems="center">
<Input type="text" placeholder="person,chair,TV" ref={inputRef} />
<Button onClick={handleSendMessage}>Update&nbsp;Classes</Button>
</Flex>
</div>
);
}
return (
<div className={css({ display: "flex", flexDirection: "column", gap: "sm" })}>
{/* Class List Display */}
<h3 className={css({ fontWeight: "semibold" })}>
Update Classes with Text Input:
</h3>

<div
className={css({
maxHeight: "150px",
overflowY: "auto",
border: "1px solid token(colors.border.subtle)",
borderRadius: "md",
padding: "sm",
backgroundColor: "token(colors.bg.surface)",
})}
>
{selectedClasses.length > 0 ? (
<ul className={css({ listStyle: "disc", pl: "lg", m: 0 })}>
{selectedClasses.map((cls, i) => (
<li key={i}>{cls}</li>
))}
</ul>
) : (
<p className={css({ color: "gray.600", fontSize: "sm" })}>No classes selected.</p>
)}
</div>

{/* Input + Button */}
<Flex direction="row" gap="sm" alignItems="center">
<Input type="text" placeholder="person,chair,TV" ref={inputRef} />
<Button onClick={handleSendMessage}>Update&nbsp;Classes</Button>
</Flex>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export function ConfidenceSlider({ initialValue = 0.5 }: ConfidenceSliderProps)
</label>
<input
type="range"
min="0"
max="1"
min="0.01"
max="0.99"
step="0.01"
value={value}
onChange={(e) => setValue(parseFloat(e.target.value))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function ImageUploader({ onDrawBBox, getNextLabel, onImagePromptAdded, ma
return (
<div className={css({ display: "flex", flexDirection: "column", gap: "sm" })}>
<h3 className={css({ fontWeight: "semibold" })}>Update Classes with Image Input:</h3>
<span className={css({ color: 'gray.600', fontSize: 'sm' })}>Important: reset view before drawing a bounding box</span>
<span className={css({ color: 'gray.600', fontSize: 'sm' })}>❗Reset the view before drawing a bounding box.</span>
{maxReached && (
<span className={css({ color: 'red.600', fontSize: 'sm' })}>
Maximum number of image prompts reached. Please delete or reset image prompts to add more.
Expand All @@ -100,7 +100,7 @@ export function ImageUploader({ onDrawBBox, getNextLabel, onImagePromptAdded, ma
_hover: { backgroundColor: maxReached ? "gray.50" : "gray.100" },
})}
>
{selectedFile ? selectedFile.name : "Click here to choose an image file"}
{selectedFile ? selectedFile.name : "Click here to choose an image file."}
</label>

{/* Hidden file input */}
Expand All @@ -114,21 +114,34 @@ export function ImageUploader({ onDrawBBox, getNextLabel, onImagePromptAdded, ma
/>

{/* Upload / Draw buttons */}
<Flex direction="row" gap="sm" alignItems="center">
<Button onClick={handleUpload} disabled={maxReached}>Upload Image</Button>
<span className={css({ color: 'gray.500' })}>OR</span>
<Button
variant="outline"
onClick={() => {
console.log("[BBox] Button clicked: enabling drawing overlay");
onDrawBBox?.();
notify('Drawing mode enabled. Drag on the stream to draw a box.', { type: 'info', durationMs: 6000 });
}}
disabled={maxReached}
>
Draw bounding box
</Button>
<Flex
direction="row"
alignItems="center"
justify="center"
gap="md"
className={css({ marginTop: "md" })}
>
<Button onClick={handleUpload} disabled={maxReached}>
Upload Image
</Button>

<span>or</span>

<Button
onClick={() => {
console.log("[BBox] Button clicked: enabling drawing overlay");
onDrawBBox?.();
notify("Drawing mode enabled. Drag on the stream to draw a box.", {
type: "info",
durationMs: 6000,
});
}}
disabled={maxReached}
>
Draw Bounding Box
</Button>
</Flex>

</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,45 +55,63 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
function Toast({ notification, onClose, index }: { notification: Notification; onClose: () => void; index: number }) {
const { message, type } = notification;
const colorMap: Record<NonNullable<Notification["type"]>, { bg: string; border: string; text: string }> = {
info: { bg: "blue.50", border: "blue.300", text: "blue.900" },
success: { bg: "green.50", border: "green.300", text: "green.900" },
warning: { bg: "yellow.50", border: "yellow.300", text: "yellow.900" },
error: { bg: "red.50", border: "red.300", text: "red.900" },
info: { bg: "white", border: "blue.400", text: "blue.900" },
success: { bg: "white", border: "green.400", text: "green.900" },
warning: { bg: "white", border: "yellow.400", text: "yellow.900" },
error: { bg: "white", border: "red.400", text: "red.900" },
};
const colors = colorMap[type ?? "info"];

return (
<div className={css({
backgroundColor: colors.bg,
border: "1px solid",
borderColor: colors.border,
color: colors.text,
borderRadius: "lg",
paddingX: "4",
paddingY: "3",
width: "60%",
boxShadow: "xl",
pointerEvents: "auto",
transform: "translateY(8px)",
animation: "slideInUp 180ms ease-out forwards",
_motionSafe: {
<div
className={css({
backgroundColor: colors.bg,
border: "1px solid",
borderColor: colors.border,
color: colors.text,
borderRadius: "lg",
paddingX: "4",
paddingY: "3",
width: "60%",
boxShadow: "xl",
pointerEvents: "auto",
transform: "translateY(8px)",
animation: "slideInUp 180ms ease-out forwards",
},
wordBreak: "break-word",
boxSizing: "border-box",
})}
_motionSafe: { animation: "slideInUp 180ms ease-out forwards" },
wordBreak: "break-word",
boxSizing: "border-box",
opacity: 1,
})}
style={{ animationDelay: `${index * 30}ms` }}
>
<div className={css({ display: "flex", alignItems: "center", gap: "3" })}>
<span className={css({ fontWeight: "medium" })}>{message}</span>
<button className={css({ marginLeft: "auto", color: colors.text, _hover: { opacity: 0.8 } })} onClick={onClose}>

{/* Close button */}
<button
className={css({
marginLeft: "auto",
color: colors.text,
fontSize: "lg",
fontWeight: "bold",
lineHeight: "1",
background: "transparent",
border: "none",
cursor: "pointer",
padding: "0 4px",
_hover: { opacity: 0.7, transform: "scale(1.1)" },
transition: "transform 0.15s ease",
})}
onClick={onClose}
>
×
</button>
</div>
</div>
);
}


// Keyframes for smooth appear (no opacity change to keep background fully opaque)
const styleEl = (typeof document !== 'undefined') ? document.createElement('style') : null;
if (styleEl && !document.getElementById('notif-keyframes')) {
Expand Down
6 changes: 3 additions & 3 deletions custom-frontend/open-vocabulary-object-detection/oakapp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
identifier = "com.example.custom-frontend.open-vocabulary-object-detection"
identifier = "com.luxonis.custom-frontend.open-vocabulary-object-detection"
entrypoint = ["bash", "-c", "/usr/bin/runsvdir -P /etc/service"]
app_version = "1.0.0"
app_version = "1.0.3"

prepare_container = [
{ type = "COPY", source = "./backend/src/requirements.txt", target = "./backend/src/requirements.txt" },
Expand Down Expand Up @@ -30,4 +30,4 @@ oauth_url = "https://auth.docker.io/token"
auth_type = "repository"
auth_name = "luxonis/oakapp-base"
image_name = "luxonis/oakapp-base"
image_tag = "1.2.4"
image_tag = "1.2.6"
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CREStereo: Depth Estimation

This example compares depth output of neural stereo matching using [CREStereo](https://models.luxonis.com/luxonis/crestereo/4729a8bd-54df-467a-92ca-a8a5e70b52ab) to output of stereo disparity. The model is not yet quantized for RVC4, thus is executed on cpu and is slower. The example works on both RVC2 and RVC4.
This example compares depth output of neural stereo matching using [CREStereo](https://models.luxonis.com/luxonis/crestereo/4729a8bd-54df-467a-92ca-a8a5e70b52ab) to output of stereo disparity. The example works on both RVC2 and RVC4.

There are 2 available model variants for the [CREStereo](https://models.luxonis.com/luxonis/crestereo/4729a8bd-54df-467a-92ca-a8a5e70b52ab) model for each platform. If you choose to use a different model please adjust `fps_limit` argument accordingly.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ You can read more about spatial detection network in our [documentation](https:/

## Demo

![Exampe](media/spatial-detections.gif)
![Example](media/spatial-detections.gif)

## Usage

Expand Down