Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Histogram equalization feature #3447

Merged
merged 12 commits into from
Jul 27, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to export/import tasks (<https://github.com/openvinotoolkit/cvat/pull/3056>)
- Add a tutorial for semi-automatic/automatic annotation (<https://github.com/openvinotoolkit/cvat/pull/3124>)
- Explicit "Done" button when drawing any polyshapes (<https://github.com/openvinotoolkit/cvat/pull/3417>)
- Histogram equalization with OpenCV javascript (<https://github.com/openvinotoolkit/cvat/pull/3447>)

### Changed

Expand Down
8 changes: 6 additions & 2 deletions cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface Configuration {
showProjections?: boolean;
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
}

export interface DrawData {
Expand Down Expand Up @@ -392,8 +393,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
}

if (frameData.number === this.data.imageID) {
if (frameData.number === this.data.imageID && !this.data.configuration.forceFrameUpdate) {
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
Expand Down Expand Up @@ -652,6 +652,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.intelligentPolygonCrop = configuration.intelligentPolygonCrop;
}

if (typeof configuration.forceFrameUpdate === 'boolean') {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
}

this.notify(UpdateReasons.CONFIG_UPDATED);
}

Expand Down
2 changes: 2 additions & 0 deletions cvat-core/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function build() {
const { Project } = require('./project');
const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model');
const { FrameData } = require('./frames');

const enums = require('./enums');

Expand Down Expand Up @@ -765,6 +766,7 @@ function build() {
Comment,
Issue,
Review,
FrameData,
},
};

Expand Down
8 changes: 8 additions & 0 deletions cvat-core/src/frames.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@
const result = await PluginRegistry.apiWrapper.call(this, FrameData.prototype.data, onServerRequest);
return result;
}

get imageData() {
return this._data.imageData;
}

set imageData(imageData) {
this._data.imageData = imageData;
}
}

FrameData.prototype.data.implementation = async function (onServerRequest) {
Expand Down
6 changes: 3 additions & 3 deletions cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,8 @@ export function getPredictionsAsync(): ThunkAction {
};
}

export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameStep?: number): ThunkAction {
export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameStep?: number,
forceUpdate?: boolean): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job;
Expand All @@ -700,7 +701,7 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte
throw Error(`Required frame ${toFrame} is out of the current job`);
}

if (toFrame === frame) {
if (toFrame === frame && !forceUpdate) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
Expand All @@ -719,7 +720,6 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte

return;
}

// Start async requests
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
import Popover from 'antd/lib/popover';
import Icon, { ScissorOutlined } from '@ant-design/icons';
import Icon, { AreaChartOutlined, ScissorOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs';
import Button from 'antd/lib/button';
Expand All @@ -26,9 +26,11 @@ import {
fetchAnnotationsAsync,
updateAnnotationsAsync,
createAnnotationsAsync,
changeFrameAsync,
} from 'actions/annotation-actions';
import LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip';
import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces';
import withVisibilityHandling from './handle-popover-visibility';

interface Props {
Expand All @@ -39,20 +41,28 @@ interface Props {
states: any[];
frame: number;
curZOrder: number;
frameData: any;
}

interface DispatchToProps {
onInteractionStart(activeInteractor: OpenCVTool, activeLabelID: number): void;
updateAnnotations(statesToUpdate: any[]): void;
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void;
changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void;
}

interface State {
libraryInitialized: boolean;
initializationError: boolean;
initializationProgress: number;
activeLabelID: number;
activeImageModifiers: ImageModifier[];
}

interface ImageModifier {
modifier: ImageProcessing,
alias: string
}

const core = getCore();
Expand All @@ -68,7 +78,7 @@ function mapStateToProps(state: CombinedState): Props {
job: { instance: jobInstance, labels },
canvas: { activeControl, instance: canvasInstance },
player: {
frame: { number: frame },
frame: { number: frame, data: frameData },
},
},
} = state;
Expand All @@ -81,6 +91,7 @@ function mapStateToProps(state: CombinedState): Props {
labels,
states,
frame,
frameData,
};
}

Expand All @@ -89,26 +100,32 @@ const mapDispatchToProps = {
updateAnnotations: updateAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync,
changeFrame: changeFrameAsync,
};

class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> {
private activeTool: IntelligentScissors | null;
private canvasForceUpdateWasEnabled: boolean;

public constructor(props: Props & DispatchToProps) {
super(props);
const { labels } = props;
this.activeTool = null;
this.canvasForceUpdateWasEnabled = false;

this.state = {
libraryInitialized: openCVWrapper.isInitialized,
initializationError: false,
initializationProgress: -1,
activeLabelID: labels.length ? labels[0].id : null,
activeImageModifiers: [],
};
}

public componentDidMount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().addEventListener('canvas.setup', this.runImageModifier);
}

public componentDidUpdate(prevProps: Props): void {
Expand All @@ -124,6 +141,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public componentWillUnmount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener);
canvasInstance.html().removeEventListener('canvas.setup', this.runImageModifier);
}

private interactionListener = async (e: Event): Promise<void> => {
Expand Down Expand Up @@ -173,6 +191,42 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
}
};

private runImageModifier = async ():Promise<void> => {
const { activeImageModifiers } = this.state;
const {
frameData, states, curZOrder, canvasInstance, frame,
} = this.props;
try {
if (activeImageModifiers.length !== 0 && activeImageModifiers[0].modifier.currentProcessedImage !== frame) {
this.enableCanvasForceUpdate();
const canvas: HTMLCanvasElement | undefined = window.document.getElementById('cvat_canvas_background') as
| HTMLCanvasElement
| undefined;
if (!canvas) {
throw new Error('Element #cvat_canvas_background was not found');
}
const { width, height } = canvas;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context is empty');
}
const imageData = context.getImageData(0, 0, width, height);
const newImageData = activeImageModifiers.reduce((oldImageData, activeImageModifier) =>
activeImageModifier.modifier.processImage(oldImageData, frame), imageData);
const imageBitmap = await createImageBitmap(newImageData);
frameData.imageData = imageBitmap;
canvasInstance.setup(frameData, states, curZOrder);
}
} catch (error) {
notification.error({
description: error.toString(),
message: 'OpenCV.js processing error occured',
});
} finally {
this.disableCanvasForceUpdate();
}
};

private async runCVAlgorithm(pressedPoints: number[], threshold: number): Promise<number[]> {
// Getting image data
const canvas: HTMLCanvasElement | undefined = window.document.getElementById('cvat_canvas_background') as
Expand Down Expand Up @@ -215,6 +269,45 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
return points;
}

private imageModifier(alias: string): ImageProcessing|null {
const { activeImageModifiers } = this.state;
return activeImageModifiers.find((imageModifier) => imageModifier.alias === alias)?.modifier || null;
}

private disableImageModifier(alias: string):void {
const { activeImageModifiers } = this.state;
const index = activeImageModifiers.findIndex((imageModifier) => imageModifier.alias === alias);
if (index !== -1) {
activeImageModifiers.splice(index, 1);
this.setState({
activeImageModifiers: [...activeImageModifiers],
});
}
}

private enableImageModifier(modifier: ImageProcessing, alias: string): void{
this.setState((prev: State) => ({
...prev,
activeImageModifiers: [...prev.activeImageModifiers, { modifier, alias }],
}), () => {
this.runImageModifier();
});
}

private enableCanvasForceUpdate():void{
const { canvasInstance } = this.props;
canvasInstance.configure({ forceFrameUpdate: true });
this.canvasForceUpdateWasEnabled = true;
}

private disableCanvasForceUpdate():void{
if (this.canvasForceUpdateWasEnabled) {
const { canvasInstance } = this.props;
canvasInstance.configure({ forceFrameUpdate: false });
this.canvasForceUpdateWasEnabled = false;
}
}

private renderDrawingContent(): JSX.Element {
const { activeLabelID } = this.state;
const { labels, canvasInstance, onInteractionStart } = this.props;
Expand Down Expand Up @@ -254,6 +347,36 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
);
}

private renderImageContent():JSX.Element {
return (
<Row justify='start'>
<Col>
<CVATTooltip title='Histogram equalization' className='cvat-opencv-image-tool'>
<Button
className={this.imageModifier('histogram') ? 'cvat-opencv-image-tool-active' : ''}
onClick={(e: React.MouseEvent<HTMLElement>) => {
const modifier = this.imageModifier('histogram');
if (!modifier) {
this.enableImageModifier(openCVWrapper.imgproc.hist(), 'histogram');
} else {
const button = e.target as HTMLElement;
button.blur();
this.disableImageModifier('histogram');
const { changeFrame } = this.props;
const { frame } = this.props;
this.enableCanvasForceUpdate();
changeFrame(frame, false, 1, true);
bsekachev marked this conversation as resolved.
Show resolved Hide resolved
}
}}
>
<AreaChartOutlined />
</Button>
</CVATTooltip>
</Col>
</Row>
);
}

private renderContent(): JSX.Element {
const { libraryInitialized, initializationProgress, initializationError } = this.state;

Expand All @@ -271,7 +394,9 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
<Tabs.TabPane key='drawing' tab='Drawing' className='cvat-opencv-control-tabpane'>
{this.renderDrawingContent()}
</Tabs.TabPane>
<Tabs.TabPane disabled key='image' tab='Image' className='cvat-opencv-control-tabpane' />
<Tabs.TabPane key='image' tab='Image' className='cvat-opencv-control-tabpane'>
{this.renderImageContent()}
</Tabs.TabPane>
</Tabs>
) : (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@
}
}

.cvat-opencv-image-tool {
@extend .cvat-opencv-drawing-tool;
}

.cvat-opencv-image-tool-active {
color: #40a9ff;
border-color: #40a9ff;
}

.cvat-setup-tag-popover-content,
.cvat-draw-shape-popover-content {
padding: $grid-unit-size;
Expand Down
Loading