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

Feature/contrast adjustment and image zoom issue #8800 #8881

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import defaultLayout, { ItemLayout, ViewType } from './canvas-layout.conf';

const ReactGridLayout = WidthProvider(RGL);

const ViewFabric = (itemLayout: ItemLayout): JSX.Element => {
const ViewFabric = (itemLayout: ItemLayout, fullscreenKey?: string): JSX.Element => {
const { viewType: type, offset } = itemLayout;

let component = null;
Expand All @@ -49,7 +49,7 @@ const ViewFabric = (itemLayout: ItemLayout): JSX.Element => {
component = <PerspectiveViewComponent />;
break;
case ViewType.RELATED_IMAGE:
component = <ContextImage offset={offset} />;
component = <ContextImage offset={offset} fullscreenKey={fullscreenKey} />;
break;
case ViewType.CANVAS_3D_FRONT:
component = <FrontViewComponent />;
Expand Down Expand Up @@ -201,7 +201,7 @@ function CanvasLayout({ type }: { type?: DimensionType }): JSX.Element {
window.dispatchEvent(new Event('resize'));
}, [layoutConfig]);

const children = layoutConfig.map((value: ItemLayout) => ViewFabric(value));
const children = layoutConfig.map((value: ItemLayout) => ViewFabric(value, fullscreenKey));
const layout = layoutConfig.map((value: ItemLayout) => ({
x: value.x,
y: value.y,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {

public componentWillUnmount(): void {
const { canvasInstance } = this.props as { canvasInstance: Canvas };

canvasInstance.html().removeEventListener('mousemove', this.onCanvasMouseMove);
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart);
Expand Down Expand Up @@ -746,6 +746,25 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
onStartIssue(points);
};

private onCanvasMouseMove = (e: MouseEvent): void => {
if (e.buttons === 2) {
const {
onChangeBrightnessLevel,
onChangeContrastLevel,
brightnessLevel,
contrastLevel,
} = this.props;

const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));

const newBrightness = clamp((brightnessLevel * 100) + (e.movementX / 2), 50, 200);
const newContrast = clamp((contrastLevel * 100) + (e.movementY / 2), 50, 200);

onChangeBrightnessLevel(newBrightness);
onChangeContrastLevel(newContrast);
}
};

private onCanvasMouseDown = (e: MouseEvent): void => {
const { workspace, activatedStateID, onActivateObject } = this.props;

Expand Down Expand Up @@ -1056,6 +1075,7 @@ class CanvasWrapperComponent extends React.PureComponent<Props> {
{ once: true },
);

canvasInstance.html().addEventListener('mousemove', this.onCanvasMouseMove);
canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().addEventListener('click', this.onCanvasClicked);
canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import PropTypes from 'prop-types';
import notification from 'antd/lib/notification';
import Spin from 'antd/lib/spin';
import Text from 'antd/lib/typography/Text';
import { SettingOutlined } from '@ant-design/icons';
import { ReloadOutlined, SettingOutlined } from '@ant-design/icons';

import CVATTooltop from 'components/common/cvat-tooltip';
import { CombinedState } from 'reducers';
import ContextImageSelector from './context-image-selector';
import { useCanvasControl } from './model';

interface Props {
offset: number[];
fullscreenKey?: string;
}

function ContextImage(props: Props): JSX.Element {
const { offset } = props;
const { offset, fullscreenKey } = props;
const defaultFrameOffset = (offset[0] || 0);
const defaultContextImageOffset = (offset[1] || 0);

Expand All @@ -38,6 +40,18 @@ function ContextImage(props: Props): JSX.Element {
const [hasError, setHasError] = useState<boolean>(false);
const [showSelector, setShowSelector] = useState<boolean>(false);

const {
canvasStyle,
handleContextMenu,
handleMouseDown,
handleMouseLeave,
handleMouseMove,
handleMouseUp,
handleZoomChange,
resetColorSetting,
wrapperRef,
} = useCanvasControl(canvasRef, fullscreenKey);

useEffect(() => {
let unmounted = false;
const promise = job.frames.contextImage(frameIndex);
Expand Down Expand Up @@ -82,7 +96,12 @@ function ContextImage(props: Props): JSX.Element {

const contextImageName = Object.keys(contextImageData).sort()[contextImageOffset];
return (
<div className='cvat-context-image-wrapper'>
<div
className='cvat-context-image-wrapper'
onWheel={handleZoomChange}
ref={wrapperRef}
onMouseLeave={handleMouseLeave}
>
<div className='cvat-context-image-header'>
{ relatedFiles > 1 && (
<SettingOutlined
Expand All @@ -92,6 +111,10 @@ function ContextImage(props: Props): JSX.Element {
}}
/>
)}
<ReloadOutlined
className='cvat-context-image-reset-button'
onClick={resetColorSetting}
/>
<div className='cvat-context-image-title'>
<CVATTooltop title={contextImageName}>
<Text>{contextImageName}</Text>
Expand All @@ -102,8 +125,18 @@ function ContextImage(props: Props): JSX.Element {
(!fetching && contextImageOffset >= Object.keys(contextImageData).length)) && <Text> No data </Text>}
{ fetching && <Spin size='small' /> }
{
contextImageOffset < Object.keys(contextImageData).length &&
<canvas ref={canvasRef} />
contextImageOffset < Object.keys(contextImageData).length && (
<div className='draggable-wrapper'>
<canvas
ref={canvasRef}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onContextMenu={handleContextMenu}
style={canvasStyle}
/>
</div>
)
}
{ showSelector && (
<ContextImageSelector
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) 2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

export { useCanvasControl } from './useCanvasControl';
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (C) 2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import {
RefObject,
useCallback, useEffect, useMemo, useRef, useState,
} from 'react';

interface Position {
x: number;
y: number;
}
export const useCanvasControl = (canvasRef: RefObject<HTMLCanvasElement>, fullscreenKey: string | undefined) => {
const [zoomLevel, setZoomLevel] = useState<number>(1);

const [isDragging, setIsDragging] = useState<boolean>(false);
const [dragOffset, setDragOffset] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
const positionRef = useRef<Position>({ x: 0, y: 0 });
const animationFrameRef = useRef<number | null>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [brightness, setBrightness] = useState<number>(100);
const [contrast, setContrast] = useState<number>(100);

useEffect(() => {
positionRef.current = { x: 0, y: 0 };
if (canvasRef.current) {
canvasRef.current.style.transform = `scale(${zoomLevel}) translate(0px, 0px)`;
}
}, [fullscreenKey]);

const resetColorSetting = () => {
setBrightness(100);
setContrast(100);
};
const handleZoomChange = useCallback(
(event: React.WheelEvent) => {
const delta = event.deltaY;
const newZoomLevel = zoomLevel - delta * 0.001;
setZoomLevel(Math.max(0.5, Math.min(5, newZoomLevel)));
},
[zoomLevel],
);

const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
if (e.buttons === 1 && isDragging && canvasRef.current) {
const newPosition = {
x: (e.clientX - dragOffset.x) / zoomLevel,
y: (e.clientY - dragOffset.y) / zoomLevel,
};
positionRef.current = newPosition;
if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
if (canvasRef.current) {
canvasRef.current.style.transform = `
scale(${zoomLevel}) translate(${positionRef.current.x}px, ${positionRef.current.y}px)`;
}
animationFrameRef.current = null;
});
} else if (e.buttons === 2) {
const deltaX = e.movementX;
const deltaY = e.movementY;

const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));

if (deltaY !== 0) {
setContrast((prevContrast) => clamp(prevContrast + deltaY / 2, 50, 200));
}

if (deltaX !== 0) {
setBrightness((prevBrightness) => clamp(prevBrightness + deltaX / 2, 50, 200));
}
}
}, [zoomLevel, isDragging, dragOffset]);

const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
if (e.button === 0) {
setIsDragging(true);
setDragOffset({
x: e.clientX - positionRef.current.x * zoomLevel,
y: e.clientY - positionRef.current.y * zoomLevel,
});
if (canvasRef.current) {
canvasRef.current.style.cursor = 'grabbing';
}
}
},
[zoomLevel],
);

const canvasStyle = useMemo(
() => ({
transform: `scale(${zoomLevel}) translate(${positionRef.current.x}px, ${positionRef.current.y}px)`,
transformOrigin: 'center',
cursor: isDragging ? 'grabbing' : 'grab',
transition: isDragging ? 'none' : 'transform 0.1s ease-out',
filter: `brightness(${brightness}%) contrast(${contrast}%)`,
}),
[zoomLevel, isDragging, brightness, contrast],
);

const handleMouseUp = useCallback(() => {
setIsDragging(false);
if (canvasRef.current) {
canvasRef.current.style.cursor = 'grab';
}
if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
}, []);

const handleMouseLeave = useCallback(() => {
// setIsDragging(false);
// if (canvasRef.current) {
// canvasRef.current.style.cursor = 'grab';
// }
}, []);

const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault();
}, []);

return {
handleContextMenu,
handleZoomChange,
handleMouseLeave,
handleMouseUp,
wrapperRef,
canvasStyle,
handleMouseDown,
handleMouseMove,
resetColorSetting,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) 2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

export { useCanvasControl } from './hooks';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
height: 100%;
display: flex;
justify-content: center;
overflow: hidden;
border-radius: 10px 10px 0 0;

> .ant-spin {
position: absolute;
Expand Down Expand Up @@ -60,15 +62,33 @@
top: $grid-unit-size;
right: $grid-unit-size;
}

> .cvat-context-image-reset-button{
position: absolute;
opacity: 0.6;
top: $grid-unit-size * 1.1;
left: $grid-unit-size * 7;

&:hover{
opacity: 1;
}
}
}

> canvas {
object-fit: contain;
.draggable-wrapper{
position: relative;
top: calc(50% + $grid-unit-size * 2);
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
width: 100%;
height: calc(100% - $grid-unit-size * 4);

> canvas {
object-fit: contain;
position: absolute;
width: 100%;
height: 100%;
}
}

&:hover {
Expand Down