Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A lightweight bounding box annotation tool for image datasets. Built with React/
- 🖼️ **Multi-format support** — PNG, JPEG, WebP, BMP
- 📁 **Project management** — Organize annotations by project
- 🏷️ **Custom labels** — Define your own class labels
- 🎨 **Adaptive box colors** — Auto contrast, label-based, or custom color modes
- ⌨️ **Keyboard shortcuts** — Fast annotation workflow
- 📤 **Multiple export formats** — YOLO, COCO, Pascal VOC, CreateML, CSV
- 🔄 **Train/Val/Test split** — Automatic dataset splitting for YOLO export
Expand All @@ -36,9 +37,17 @@ pip install bbannotate
# Start the annotation server
bbannotate start

# Opens http://127.0.0.1:8000 in your browser
# Opens http://127.0.0.1:8000 in your browser.
# By default the server runs detached, so you can close the terminal.
# Closing that browser session stops the detached server automatically.
```

### Runtime Modes

- **Default (`bbannotate start`)**: Starts server in detached mode and links lifecycle to the opened browser session.
- **Development (`bbannotate start --reload`)**: Runs in foreground with auto-reload.
- **No Browser (`bbannotate start --no-browser`)**: Starts server without opening browser automatically.

### CLI Options

```bash
Expand All @@ -51,15 +60,15 @@ Options:
-r, --reload Enable auto-reload for development
-d, --data-dir PATH Directory for storing data [default: ./data]
--projects-dir PATH Directory for storing projects [default: ./projects]
-v, --version Show version and exit
--help Show help and exit
```

### Other Commands

```bash
bbannotate info # Show installation info
bbannotate build-frontend # Build frontend assets (development)
bbannotate info # Show installation info
bbannotate status # Show runtime status (backend/frontend processes + ports)
bbannotate build-frontend # Build frontend assets (development)
```

## Keyboard Shortcuts
Expand All @@ -74,6 +83,7 @@ bbannotate build-frontend # Build frontend assets (development)
| `Del` / `Backspace` | Delete annotation |
| `⌘Z` / `Ctrl+Z` | Undo last annotation |
| `Esc` | Deselect / Cancel |
| `Enter` | Mark image done |

## Export Formats

Expand Down Expand Up @@ -134,7 +144,7 @@ frontend/ # React/TypeScript frontend
src/
components/ # UI components
hooks/ # React hooks
tests/ # Test suite (145 tests)
tests/ # Python test suite
```

## License
Expand Down
7 changes: 7 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Todo in this version
- Create picture outline in view (DONE)
- Create ability to rename a project (DONE)
- Add tool to toolbar inside picture view that allows to change color of bounding box or better yet - automatically determine color that has high contrast compared to image.
- Make sure that terminal can be closed after `bbannotate start` without disrupting program
- Make sure closing browser window stops all processes.
- When pressing bb item in right hand side panel the highlighting of the selected bounding box on the image should be clearer (easier to see)
36 changes: 18 additions & 18 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 61 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ import {
closeProject,
markImageDone,
getAllDoneStatus,
sendBrowserSessionHeartbeat,
sendBrowserSessionClose,
} from '@/lib/api';
import type { ToolMode, DrawingRect, BoundingBox, Project } from '@/types';
import type { ToolMode, DrawingRect, BoundingBox, Project, BoundingBoxColorMode } from '@/types';

/** Default labels - empty so users define their own */
const DEFAULT_LABELS: string[] = [];
const DEFAULT_CUSTOM_BBOX_COLOR = '#22c55e';
const BROWSER_SESSION_HEARTBEAT_MS = 2500;

/** Get the localStorage key for a project's labels */
function getLabelsKey(projectName: string | null): string {
Expand Down Expand Up @@ -64,6 +68,19 @@ function saveLabelsForProject(projectName: string | null, labels: string[]): voi
function App(): JSX.Element {
const [currentProject, setCurrentProject] = useState<Project | null>(null);
const [toolMode, setToolMode] = useState<ToolMode>('draw');
const [bboxColorMode, setBboxColorMode] = useState<BoundingBoxColorMode>(() => {
if (typeof window === 'undefined') return 'auto';
const saved = localStorage.getItem('bboxColorMode');
if (saved === 'label' || saved === 'auto' || saved === 'custom') {
return saved;
}
return 'auto';
});
const [customBboxColor, setCustomBboxColor] = useState<string>(() => {
if (typeof window === 'undefined') return DEFAULT_CUSTOM_BBOX_COLOR;
const saved = localStorage.getItem('customBboxColor');
return saved ?? DEFAULT_CUSTOM_BBOX_COLOR;
});
const [labels, setLabels] = useState<string[]>(DEFAULT_LABELS);
const [currentLabel, setCurrentLabel] = useState<string>('');
const [showLabelManager, setShowLabelManager] = useState(false);
Expand Down Expand Up @@ -182,6 +199,45 @@ function App(): JSX.Element {
}
}, [darkMode]);

// Persist bounding box color preferences
useEffect(() => {
localStorage.setItem('bboxColorMode', bboxColorMode);
}, [bboxColorMode]);

useEffect(() => {
localStorage.setItem('customBboxColor', customBboxColor);
}, [customBboxColor]);

// Keep detached server alive while browser session is active
useEffect(() => {
const sessionToken = new URLSearchParams(window.location.search).get('bb_session');
if (!sessionToken) {
return;
}

const sendHeartbeat = (): void => {
void sendBrowserSessionHeartbeat(sessionToken).catch(() => {
// Best-effort lifecycle signal - safe to ignore transient failures
});
};

const handleCloseSignal = (): void => {
sendBrowserSessionClose(sessionToken);
};

sendHeartbeat();
const intervalId = window.setInterval(sendHeartbeat, BROWSER_SESSION_HEARTBEAT_MS);
window.addEventListener('pagehide', handleCloseSignal);
window.addEventListener('beforeunload', handleCloseSignal);

return () => {
window.clearInterval(intervalId);
window.removeEventListener('pagehide', handleCloseSignal);
window.removeEventListener('beforeunload', handleCloseSignal);
handleCloseSignal();
};
}, []);

// Show errors from hooks as toasts
useEffect(() => {
if (imagesError) {
Expand Down Expand Up @@ -630,6 +686,8 @@ function App(): JSX.Element {
annotations={annotations}
selectedId={selectedId}
toolMode={toolMode}
bboxColorMode={bboxColorMode}
customBboxColor={customBboxColor}
currentLabel={currentLabel}
currentClassId={labels.indexOf(currentLabel)}
labels={labels}
Expand All @@ -639,6 +697,8 @@ function App(): JSX.Element {
onUpdateBbox={handleUpdateBbox}
onDeleteAnnotation={handleDeleteAnnotation}
onToolModeChange={setToolMode}
onBboxColorModeChange={setBboxColorMode}
onCustomBboxColorChange={setCustomBboxColor}
onMarkDone={handleMarkDone}
onLabelChange={setCurrentLabel}
/>
Expand Down
Loading