@abidibo/react-cam-roi
v0.14.4
Published
A react component for drawing ROI over images and managing metadata
Readme
React Cam ROI
This is a react component which lets you draw regions of interest (ROI) over images, manage metadata and import/export everything.
Metadata are dynamic information that can be attached as a whole and/or to each ROI. The number and type of drawable ROIs can also be configured.

It provides one component: RoiEditor and one provider: UiProvider. The editor lets you add metadata and draw regions of interest over a given image (url).
Export and import functionality is also provided.
Features:
- Autosizing of the editor: the canvas resizes to the size of the image, but it's also responsive, so if the container width is smaller, then the canvas is resized accordingly keeping the aspect ratio. (the size is adjusted on load, after will remain the same even if changing viewport size)
- Draw polylines, polygons and rectangles, change dimensions and rotate them.
- Support for number of drawable ROIs, defining a rule for each type.
- Support for dynamic metadata information attached to each shape and the whole image (with validation included).
- Import and export shapes and metadata in json format.
- Highly customizable: shape colors, custom components and css classes.
Installation
npm install @abidibo/react-cam-roiUsage
import { RoiEditor, UiProvider, Types } from '@abidibo/react-cam-roi'
import { Typography, IconButton, Delete } from '@mui/material'
import '@abidibo/react-cam-roi/dist/index.esm.css' // !Important you must include the css
const MyComponent: React.FC = () => {
const themMode = 'light'
const config = {} // see below
const handleSubmit = (data: Types.Output) => console.log(data) // onSubmit runs validation
const handleUpdate = (data: Types.Output) => console.log(data) // onUpdate runs without validation
return (
<UiProvider themeMode={themeMode} IconButton={IconButton} Typography={Typography} DeleteIcon={() => <Delete />}>
<RoiEditor imageUrl={'https://placecats.com/800/600'} configuration={config} onSubmit={handleSubmit} onUpdate={handleUpdate} />
</UiProvider>
)
}The RoiEditor props and the Output interface used both in import and export:
export type RoiEditorProps = {
// id of this editor instance, should be unique
editorId: string
// the url of the image we want to annotate
imageUrl: string
// configuration object
configuration: Configuration
// callback called when exporting data
onSubmit: (data: Output) => void
// initial imported data
initialData?: Output
// allow partial save: no error notified but errors are returned
allowPartialSave?: boolean
// slots for injecting components
slots?: {
// injected above ROIs section
roiAbove?: React.ReactNode
}
}
export const enum ToolEnum {
Pointer = 'pointer',
Point = 'point',
Polyline = 'polyline',
Polygon = 'polygon',
Rectangle = 'rect',
}
export type ShapeType = ToolEnum.Polyline | ToolEnum.Polygon | ToolEnum.Rectangle | ToolEnum.Point
export type OutputShapePoint = {
angle: number
scaleX: number
scaleY: number
skewX: number
skewY: number
top: number
left: number
color: string
}
export type OutputShapeRect = {
angle: number
scaleX: number
scaleY: number
skewX: number
skewY: number
top: number
left: number
width: number
height: number
color: string
}
export type OutputShapePolyline = {
angle: number
scaleX: number
scaleY: number
skewX: number
skewY: number
points: { x: number; y: number }[]
top: number
left: number
color: string
}
export type OutputShapePolygon = {
angle: number
scaleX: number
scaleY: number
skewX: number
skewY: number
points: { x: number; y: number }[]
top: number
left: number
color: string
}
export type OutputPointCoords = {
x: number
y: number
}
export type OutputRectCoords = {
points: { x: number; y: number }[]
}
export type OutputPolylineCoords = {
points: { x: number; y: number }[]
}
export type OutputPolygonCoords = {
points: { x: number; y: number }[]
}
export interface OutputParameter {
codename: string
value: number | string | boolean | string[] | number[] | null
}
export interface OutputRoi {
parameters: OutputParameter[]
type: ShapeType
name: string
role: string
shape: OutputShapeRect | OutputShapePolyline | OutputShapePolygon | OutputShapePoint // fabric obj coords
coords: OutputRectCoords | OutputPolylineCoords | OutputPolygonCoords | OutputPointCoords // canvas coords
}
export interface Output {
parameters: OutputParameter[]
presetName: string
presetDescription: string
rois: OutputRoi[]
errors?: string[]
}Take a look at the UiProvider allowed props (below) to see all the customization options.
Configuration
The configuration prop defines which kind and how many ROIs can be drawn, along with metadata information. Here the types definitions and an example:
// All types can be imported:
// import { Types } from '@abidibo/react-cam-roi'
// const { ToolEnum, ShapeType, DataTypeEnum, OperatorEnum, ConfigurationParameter, RoiConfiguration, Configuration } = Types
// The drawable shapes plus the Pointer tool
export const enum ToolEnum {
Pointer = 'pointer',
Polyline = 'polyline',
Polygon = 'polygon',
Rectangle = 'rect',
}
// Allowed shape types
export type ShapeType = ToolEnum.Polyline | ToolEnum.Polygon | ToolEnum.Rectangle
// Data types allowed for metadata values
export enum DataTypeEnum {
Integer = 'int',
Float = 'float',
String = 'string',
Boolean = 'bool',
}
// Operators allowed for multiplicity (control how many shapes should/can be drawn)
export enum OperatorEnum {
Lt = 'lt',
Lte = 'lte',
Gt = 'gt',
Gte = 'gte',
Eq = 'eq',
}
// Definition of a metadata parameter
export type ConfigurationParameter = {
codename: string // unique
label: string // label of the parameter
description: string // helper text
unit: string // postponed to the label
type: DataTypeEnum // value type
options?: { value: number | string | boolean; label: string }[] // if filled the component will be a dropdown
multiple?: boolean // for multiple selection
required: boolean // required parameter
value: number | string | boolean | string[] | number[] | null // default value
fieldSet?: string // fieldset for grouping fields
}
// Configuration of ROIs
export type RoiConfiguration = {
role: string // let's say the category of the roi
type: Omit<ShapeType, 'pointer'> // shape type
multiplicity: { // how many ROIs of this type can be drawn
operator: OperatorEnum
threshold: number
}
parameters: ConfigurationParameter[] // ROIs parameters for this shape type
}
// Whole configuration
export type Configuration = {
parameters: ConfigurationParameter[]
rois: RoiConfiguration[]
options: {
hideForbiddenTools?: boolean // hide tools controllers for shapes that cannot be drawn
description?: string // optional initial text shown in the editor
viewMainParameters?: boolean // show main parameters readonly form
}
}
// Example
export const configuration: Configuration = {
parameters: [
{
codename: 'analytics_1', // internal code
label: 'Analytics param 1', // to be shown in interface
description: 'This is some descriptive text', // tooltip
unit: 's', // unit
type: DataTypeEnum.Integer, // int, float, string, bool
options: [
// if filled -> enum of types type
{
value: 7,
label: 'Seven',
},
{
value: 10,
label: 'Ten',
},
],
required: true, // true | false,
value: 10, // default value
},
],
rois: [
{
role: 'invasion_area', // analytics role
type: ToolEnum.Polygon,
multiplicity: {
// how many rois of this type can/should be created
operator: OperatorEnum.Lt, // lt | lte | gt | gte | eq
threshold: 2,
},
parameters: [
{
codename: 'threshold', // internal code
label: 'Alert threshold', // to be shown in interface
description: 'Threshold used for triggering alarms', // tooltip
unit: '%', // unit
type: DataTypeEnum.Integer, // int, float, string, bool
options: [],
required: true, // true / false,
value: null, // default value
},
],
},
],
options?: {
hideForbiddenTools?: boolean,
description?: string,
viewMainParameters?: boolean
}
}UiProvider and Customization
You can customize many aspects of this library by using the UiProvider.
- You can customize both the styles and the components used in this library. The library provides default components with an interface almost compatible witu mui components (maybe you'll need to wrap some of them).
- You can override them by using the
UiProvider. But you can also use the default ones and just add your styling. - You can pass a theme mode which is used by the default components to determine the color scheme. It is also used to define custom classes you can use for styling.
- You can define a primary color which is used for color or background of active elements.
- You can define custom strings used here and there (some strings require one or more placeholders).
- You can enable logs in the console by setting the
enableLogsoption totrue.
import IconButton from '@mui/material/IconButton'
import { UiProvider, RoiEditor } from 'react-cam-roi'
const MyView: React.FC = () => {
return (
<UiProvider themeMode={'dark'} IconButton={IconButton} primaryColor={'#1976d2'} primaryFgColor={'#fff'} enableLogs>
<RoiEditor imageUrl={'whatever'} />
</UiProvider>
)
}UiProvider
Props and types are defined later in this document.
type UiContextType = {
children?: React.ReactNode
enableLogs: boolean // enable console logs
themeMode: 'light' | 'dark' // themeMode for internal components
primaryColor: string // primary color for internal components
primaryFgColor: string // text color above primary bg for internal components
Typography: React.FC<TypographyProps> // component used to surround text
Tooltip: React.FC<TooltipProps> // component used to show tooltips
Modal: React.FC<ModalProps> // modal dialog component (it displays metadata forms)
IconButton: React.FC<IconButtonProps> // wrapper for icon buttons
DeleteIcon: React.FC<DeleteIconProps> // delete icon
CopyIcon: typeof CopyIcon // copy icon (clone a shape)
AnnotateIcon: typeof AnnotateIcon // annotate icon (open metadata form)
CloseIcon: typeof CloseIcon // close icon
SaveIcon: typeof SaveIcon // save icon
TextField: typeof TextField // field used for text input
NumberField: typeof NumberField // field used for number input
BoolField: typeof BoolField // field used for boolean input
EnumField: typeof EnumField // field used for enum input (options filled in parameter definition)
Button: typeof Button // button
notify: INotify // function used to display notifications
strings: {
// strings used here and there
cancel: string
cannotDrawMorePoints: string
cannotDrawMorePolygons: string
cannotDrawMorePolylines: string
cannotDrawMoreRectangles: string
fullImage: string
id: string
invalidSubmission: string
mainParametersMetadata: string
missingPresetName: string
missingRequiredValuesInMainParameters: string
missingRequiredValuesInShapeParameters: string // with {id} placeholder
mainParametersMetadata: strings
name: string
point: string
pointHelpText: string
polygon: string
polygonHelpText: string
polyline: string
polylineHelpText: string
pointer: string
pointerHelpText: string
presetDescription: string
presetName: string
rect: string
rectHelpText: string
requiredField: string
roiMultiplicityEqRule: string // with {role}, {type} and {threshold} placeholder
roiMultiplicityGtRule: string // with {role}, {type} and {threshold} placeholder
roiMultiplicityGteRule: string // with {role}, {type} and {threshold} placeholder
roiMultiplicityLtRule: string // with {role}, {type} and {threshold} placeholder
roiMultiplicityLteRule: string // with {role}, {type} and {threshold} placeholder
roiMultiplicityNoRule: string // with {role}, {type}
roisToBeDrawn: string
role: string
save: string
selection: string
shapeParametersMetadata: string
shapesOfRoleShouldBeEqualToThreshold: string // with {role} and {threshold} placeholders
shapesOfRoleShouldBeGreaterThanThreshold: string // with {role} and {threshold} placeholders
shapesOfRoleShouldBeGreaterThanOrEqualToThreshold: string // with {role} and {threshold} placeholders
shapesOfRoleShouldBeLessThanThreshold: string // with {role} and {threshold} placeholders
shapesOfRoleShouldBeLessThanOrEqualToThreshold: string // with {role} and {threshold} placeholders
type: string
}
}Components
Here comes the list of components you can override using the UiProvider.
Loader
Interface
type LoaderProps = {}Classes
react-cam-roi-loaderreact-cam-roi-loader-lightreact-cam-roi-loader-dark
Modal
Interface
type ModalProps = {
children?: React.ReactNode
title: string
onClose: () => void
isOpen: boolean
maxWidth: 'xs' | 'sm' | 'md' | 'lg'
}Classes
react-cam-roi-modalreact-cam-roi-modal-lightreact-cam-roi-modal-darkreact-cam-roi-modal-overlayreact-cam-roi-modal-overlay-lightreact-cam-roi-modal-overlay-darkreact-cam-roi-modal-headerreact-cam-roi-modal-header-lightreact-cam-roi-modal-header-darkreact-cam-roi-modal-titlereact-cam-roi-modal-title-lightreact-cam-roi-modal-title-darkreact-cam-roi-modal-footerreact-cam-roi-modal-footer-lightreact-cam-roi-modal-footer-dark
Typography
Interface
type TypographyProps = {
children?: React.ReactNode
variant?: any // compatible with mui
component?: any // compatible with mui
className?: string
style?: React.CSSProperties
}Tooltip
Interface
type TooltipProps = {
children?: React.ReactNode
title: string
}IconButton
Interface
type IconButtonProps = {
children?: React.ReactNode
disabled?: boolean
onClick?: (event: React.MouseEvent) => void
}Classes
react-cam-roi-icon-buttonreact-cam-roi-icon-button-lightreact-cam-roi-icon-button-darkreact-cam-roi-icon-button-disabled
DeleteIcon
Interface
type DeleteIconProps = {
color?: string
style?: React.CSSProperties
}EditIcon
Interface
type EditIconProps = {
color?: string
style?: React.CSSProperties
}CopyIcon
Interface
type CopyIconProps = {
color?: string
style?: React.CSSProperties
}AnnotateIcon
Interface
type AnnotateIconProps = {
color?: string
style?: React.CSSProperties
}SaveIcon
Interface
type SaveIconProps = {
color?: string
style?: React.CSSProperties
}TextField
Interface
type TextFieldProps = {
type?: 'text' | 'email' | 'password'
onChange: (value: string) => void
value: string
label: string
helperText?: string
error?: boolean
required?: boolean
readOnly?: boolean
disabled?: boolean
fullWidth?: boolean
}Classes
react-cam-roi-text-field-wrapperreact-cam-roi-text-field-wrapper-lightreact-cam-roi-text-field-wrapper-darkreact-cam-roi-text-fieldreact-cam-roi-text-field--lightreact-cam-roi-text-field--darkreact-cam-roi-text-field--errorreact-cam-roi-text-field-labelreact-cam-roi-text-field-label-lightreact-cam-roi-text-field-label-darkreact-cam-roi-text-field-label-errorreact-cam-roi-text-field-helper-textreact-cam-roi-text-field-helper-text-lightreact-cam-roi-text-field-helper-text-darkreact-cam-roi-text-field-helper-text-error
NumberField
Interface
type NumberFieldProps = {
onChange: (value: number) => void
value: number
label: string
helperText?: string
error?: boolean
required?: boolean
readOnly?: boolean
disabled?: boolean
}Classes
react-cam-roi-number-field-wrapperreact-cam-roi-number-field-wrapper-lightreact-cam-roi-number-field-wrapper-darkreact-cam-roi-number-fieldreact-cam-roi-number-field--lightreact-cam-roi-number-field--darkreact-cam-roi-number-field--errorreact-cam-roi-number-field-labelreact-cam-roi-number-field-label-lightreact-cam-roi-number-field-label-darkreact-cam-roi-number-field-label-errorreact-cam-roi-number-field-helper-textreact-cam-roi-number-field-helper-text-lightreact-cam-roi-number-field-helper-text-darkreact-cam-roi-number-field-helper-text-error
BoolField
Interface
type BoolFieldProps = {
onChange: (value: boolean) => void
value: boolean
label: string
helperText?: string
error?: boolean
required?: boolean
readOnly?: boolean
disabled?: boolean
}Classes
react-cam-roi-bool-field-wrapperreact-cam-roi-bool-field-wrapper-lightreact-cam-roi-bool-field-wrapper-darkreact-cam-roi-bool-fieldreact-cam-roi-bool-field--lightreact-cam-roi-bool-field--darkreact-cam-roi-bool-field--errorreact-cam-roi-bool-field-labelreact-cam-roi-bool-field-label-lightreact-cam-roi-bool-field-label-darkreact-cam-roi-bool-field-label-errorreact-cam-roi-bool-field-helper-textreact-cam-roi-bool-field-helper-text-lightreact-cam-roi-bool-field-helper-text-darkreact-cam-roi-bool-field-helper-text-error
EnumField
Interface
type EnumFieldProps = {
onChange: (value: string | number | (string | number)[]) => void
value: string | number | (string | number)[]
label: string
helperText?: string
error?: boolean
required?: boolean
multiple?: boolean
disabled?: boolean
}Classes
react-cam-roi-enum-field-wrapperreact-cam-roi-enum-field-wrapper-lightreact-cam-roi-enum-field-wrapper-darkreact-cam-roi-enum-fieldreact-cam-roi-enum-field--lightreact-cam-roi-enum-field--darkreact-cam-roi-enum-field--errorreact-cam-roi-enum-field-labelreact-cam-roi-enum-field-label-lightreact-cam-roi-enum-field-label-darkreact-cam-roi-enum-field-label-errorreact-cam-roi-enum-field-helper-textreact-cam-roi-enum-field-helper-text-lightreact-cam-roi-enum-field-helper-text-darkreact-cam-roi-enum-field-helper-text-error
Button
Interface
type ButtonProps = {
onClick: (event: React.MouseEvent) => void
primary?: boolean
disabled?: boolean
}Classes
react-cam-roi-buttonreact-cam-roi-button-lightreact-cam-roi-button-darkreact-cam-roi-button-disabledreact-cam-roi-button-disabled-lightreact-cam-roi-button-disabled-dark
Functions
type INotify = {
// compatible with toast (react-toastify)
info: (message: string) => void
warn: (message: string) => void
error: (message: string) => void
success: (message: string) => void
}Styles
There are components that cannot be overridden. But still you can use classes to style them.
Top bar
react-cam-roi-top-barreact-cam-roi-top-bar-lightreact-cam-roi-top-bar-dark
Main parameters view
react-cam-roi-main-parameters-viewreact-cam-roi-main-parameters-view-lightreact-cam-roi-main-parameters-view-darkreact-cam-roi-main-parameters-buttonreact-cam-roi-main-parameters-button-lightreact-cam-roi-main-parameters-button-dark
ROIs editor wrapper
react-cam-roi-rois-wrapperreact-cam-roi-rois-wrapper-lightreact-cam-roi-rois-wrapper-dark
Canvas wrapper
react-cam-roi-canvas-wrapperreact-cam-roi-canvas-wrapper-lightreact-cam-roi-canvas-wrapper-dark
Header
react-cam-roi-headerreact-cam-roi-header-lightreact-cam-roi-header-darkreact-cam-roi-header-inforeact-cam-roi-header-info-lightreact-cam-roi-header-info-dark
Toolbar
react-cam-roi-toolbarreact-cam-roi-toolbar-lightreact-cam-roi-toolbar-darkreact-cam-roi-toolbar-spacerreact-cam-roi-toolbar-spacer-lightreact-cam-roi-toolbar-spacer-dark
Toolbar help text
react-cam-roi-toolbar-helperreact-cam-roi-toolbar-helper-lightreact-cam-roi-toolbar-helper-dark
Shapes list
react-cam-roi-shapes-tablereact-cam-roi-shapes-table-lightreact-cam-roi-shapes-table-darkreact-cam-roi-shapes-row-selected-lightreact-cam-roi-shapes-row-selected-darkreact-cam-roi-shapes-row-even-lightreact-cam-roi-shapes-row-even-darkreact-cam-roi-shapes-row-odd-lightreact-cam-roi-shapes-row-odd-dark
Colorpicker button
react-cam-roi-colorpicker-buttonreact-cam-roi-colorpicker-button-lightreact-cam-roi-colorpicker-button-darkreact-cam-roi-colorpicker-button-activereact-cam-roi-colorpicker-button-active-lightreact-cam-roi-colorpicker-button-active-dark
Form
react-cam-roi-formreact-cam-roi-fieldsetreact-cam-roi-fieldset-lightreact-cam-roi-fieldset-darkreact-cam-roi-legendreact-cam-roi-legend-lightreact-cam-roi-legend-dark
Development
After cloning the repository and install dependencies (yarn install), you can run the following commands:
| Command | Description |
| ---------------- | --------------------- |
| yarn clean | Clean the dist folder |
| yarn build | Build the library |
| yarn storybook | Run storybook |
In order to start developing just run the storybook, then make changes to code and the storybook will be updated.
In order to test the library in another local react project you can:
cd react-cam-roi
yarn link
cd ../my-project
yarn link @abidibo/react-cam-roiThen rebuild this library to see your changes in the project.
CI
A github action pipeline is provided, which is triggered by every push to the main branch.
The pipeline will publish the package to npm and update the CHANGELOG following the conventional commits.
You need to add the NODE_AUTH_TOKEN and GH_TOKEN secrets to your repository settings, see semantic-release for more information.
