Skip to content

Commit

Permalink
Histogram equalization feature (#3447)
Browse files Browse the repository at this point in the history
* Added histogram equalization

* Fixed equalization memory leak

* Removed unused console.log

* Fixed eslint errors

* Fixed algorithm implementation in opencv control

* Fixed histogram equalization disabling

* Fixed eslint errors

* Removed outdated code and reworked cycles in functions

* Fixed forceUpdate flag disabling

* Fixed image bitmap creation

* Fixed running setState and imageModifier
  • Loading branch information
klakhov authored Jul 27, 2021
1 parent e3616df commit 7e7a5b9
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 8 deletions.
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);
}
}}
>
<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

0 comments on commit 7e7a5b9

Please sign in to comment.