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

Add CLAHE to opencv image tool #8730

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Added contrast limited adaptive histogram equalization tool
(<https://github.com/cvat-ai/cvat/pull/8730>)
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import React from 'react';
import { connect } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
import Popover from 'antd/lib/popover';
import Icon, { AreaChartOutlined, ScissorOutlined } from '@ant-design/icons';
import Icon, { AreaChartOutlined, BarChartOutlined, ScissorOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs';
import Button from 'antd/lib/button';
import InputNumber from 'antd/lib/input-number';
import Progress from 'antd/lib/progress';
import Select from 'antd/lib/select';
import notification from 'antd/lib/notification';
Expand Down Expand Up @@ -84,6 +85,9 @@ interface State {
activeTracker: OpenCVTracker | null;
trackers: OpenCVTracker[];
lastTrackedFrame: number | null;
claheClipLimit: number;
claheTileColumns: number;
claheTileRows: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets create separate state object filterSettings where we can store all filter settings here.

}

const core = getCore();
Expand Down Expand Up @@ -156,6 +160,9 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
trackers: openCVWrapper.isInitialized ? Object.values(openCVWrapper.tracking) : [],
activeTracker: openCVWrapper.isInitialized ? Object.values(openCVWrapper.tracking)[0] : null,
lastTrackedFrame: null,
claheClipLimit: 40,
claheTileColumns: 8,
claheTileRows: 8,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the constants are duplicated here and in clahe.ts. Would it be possible to import the defaults directly from clahe.ts to avoid redundancy?

};
}

Expand Down Expand Up @@ -581,6 +588,8 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps

private renderImageContent():JSX.Element {
const { enableImageFilter, disableImageFilter, filters } = this.props;
const { claheClipLimit, claheTileColumns, claheTileRows } = this.state;

return (
<Row justify='start'>
<Col>
Expand All @@ -607,6 +616,118 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
</Button>
</CVATTooltip>
</Col>
<Col>
<CVATTooltip title='Contrast Limited Adaptive Histogram Equalization' className='cvat-opencv-image-tool'>
<Button
className={
hasFilter(filters, ImageFilterAlias.CLAHE) ?
'cvat-opencv-clahe-tool-button cvat-opencv-image-tool-active' : 'cvat-opencv-clahe-tool-button'
}
onClick={(e: React.MouseEvent<HTMLElement>) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for enabling the filter (and for the histogram as well) could be extracted into a separate function to reduce duplication

if (!hasFilter(filters, ImageFilterAlias.CLAHE)) {
enableImageFilter({
modifier: openCVWrapper.imgproc.clahe(),
alias: ImageFilterAlias.CLAHE,
},
{
clipLimit: claheClipLimit,
tileGridSize: {
columns: claheTileColumns,
rows: claheTileRows,
},
});
} else {
const button = e.target as HTMLElement;
button.blur();
disableImageFilter(ImageFilterAlias.CLAHE);
}
}}
klakhov marked this conversation as resolved.
Show resolved Hide resolved
>
<BarChartOutlined />
</Button>
</CVATTooltip>

{hasFilter(filters, ImageFilterAlias.CLAHE) && (
<div>
<Row align='middle' gutter={8}>
<Col span={12}>Clip Limit:</Col>
<Col span={12}>
<InputNumber
min={1}
max={255}
step={1}
value={claheClipLimit}
onChange={(value) => {
this.setState({
claheClipLimit: value || 40,
});
enableImageFilter({
modifier: openCVWrapper.imgproc.clahe(),
alias: ImageFilterAlias.CLAHE,
}, {
clipLimit: value || 40,
tileGridSize: {
columns: claheTileColumns,
rows: claheTileRows,
},
});
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reduce code duplication in parameter update handlers

The parameter update logic is repeated across multiple onChange handlers. Consider extracting this into a shared method.

+ private updateCLAHEParameters(
+   paramName: 'claheClipLimit' | 'claheTileColumns' | 'claheTileRows',
+   value: number
+ ): void {
+   const defaultValues = {
+     claheClipLimit: 40,
+     claheTileColumns: 8,
+     claheTileRows: 8
+   };
+
+   this.setState({
+     [paramName]: value || defaultValues[paramName]
+   });
+
+   this.enableImageFilter({
+     modifier: openCVWrapper.imgproc.clahe(),
+     alias: ImageFilterAlias.CLAHE,
+   }, {
+     clipLimit: paramName === 'claheClipLimit' ? value : this.state.claheClipLimit,
+     tileGridSize: {
+       columns: paramName === 'claheTileColumns' ? value : this.state.claheTileColumns,
+       rows: paramName === 'claheTileRows' ? value : this.state.claheTileRows,
+     },
+   });
+ }

- onChange={(value) => {
-   this.setState({
-     claheClipLimit: value || 40,
-   });
-   enableImageFilter({
-     modifier: openCVWrapper.imgproc.clahe(),
-     alias: ImageFilterAlias.CLAHE,
-   }, {
-     clipLimit: value || 40,
-     tileGridSize: {
-       columns: claheTileColumns,
-       rows: claheTileRows,
-     },
-   });
- }}
+ onChange={(value) => this.updateCLAHEParameters('claheClipLimit', value)}

Also applies to: 685-699, 710-724

/>
</Col>
</Row>
<Row align='middle' gutter={8}>
<Col span={12}>Tile Rows:</Col>
<Col span={12}>
<InputNumber
min={1}
max={128}
value={claheTileColumns}
onChange={(value) => {
this.setState({
claheTileColumns: value || 8,
});
enableImageFilter({
modifier: openCVWrapper.imgproc.clahe(),
alias: ImageFilterAlias.CLAHE,
}, {
clipLimit: claheClipLimit,
tileGridSize: {
columns: value || 8,
rows: claheTileRows,
},
});
}}
/>
</Col>
</Row>
<Row align='middle' gutter={8}>
<Col span={12}>Tile Columns:</Col>
<Col span={12}>
<InputNumber
min={1}
max={128}
value={claheTileRows}
onChange={(value) => {
this.setState({
claheTileRows: value || 8,
});
enableImageFilter({
modifier: openCVWrapper.imgproc.clahe(),
alias: ImageFilterAlias.CLAHE,
}, {
clipLimit: claheClipLimit,
tileGridSize: {
columns: claheTileColumns,
rows: value || 8,
},
});
}}
/>
</Col>
</Row>
</div>
)}
</Col>
</Row>
);
}
Expand Down
1 change: 1 addition & 0 deletions cvat-ui/src/utils/image-processing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ImageFilter {

export enum ImageFilterAlias {
HISTOGRAM_EQUALIZATION = 'opencv.histogramEqualizaton',
CLAHE = 'opencv.CLAHE',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Based on the search results, I can now provide a final response:

The CLAHE implementation is properly integrated but lacks test coverage

The verification shows:

  • CLAHE is correctly implemented in cvat-ui/src/utils/opencv-wrapper/clahe.ts
  • It's properly integrated in the UI controls (opencv-control.tsx)
  • The enum is used consistently with the implementation
  • However, no test files were found for this feature

Action items:

  • Add unit tests for the CLAHE implementation in cvat-ui/src/utils/opencv-wrapper/clahe.ts
  • Add integration tests for the OpenCV controls component
  • Consider adding visual regression tests for the CLAHE filter effects
🔗 Analysis chain

LGTM! Verify consistent usage across the codebase.

The new CLAHE enum value follows the established naming and value format conventions.

Let's verify the consistent usage of this new filter alias:


Let me gather more information about the CLAHE implementation to verify its usage.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent usage of the CLAHE filter alias
# Expected: Should find references in OpenCV control component and tests

# Search for CLAHE usage
rg -l "opencv\.CLAHE"

# Search for potential test files
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts

Length of output: 113


Script:

#!/bin/bash
# Search for CLAHE-related code in the codebase
rg -g '!*.test.*' -g '!*.spec.*' "CLAHE" -A 5 -B 5

# Look for image processing related components
fd -e tsx -e ts | rg -i "image|filter|opencv"

# Check for any test files containing image processing related terms
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts | rg -i "image|filter|opencv"

Length of output: 19892

GAMMA_CORRECTION = 'fabric.gammaCorrection',
}

Expand Down
81 changes: 81 additions & 0 deletions cvat-ui/src/utils/opencv-wrapper/clahe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import { BaseImageFilter, ImageProcessing } from 'utils/image-processing';

export interface CLAHE extends ImageProcessing {
processImage: (src: ImageData, frameNumber: number) => ImageData;
}

export default class CLAHEImplementation extends BaseImageFilter {
private cv:any;
// same defaults as opencv
private clipLimit: number = 40.0;
private tileGridSize: { rows: number, columns: number } = { rows: 8, columns: 8 };

constructor(cv:any) {
super();
this.cv = cv;
}

public configure(options: {
clipLimit?: number,
tileGridSize?: { width: number, height: number }
}): void {
if (options.clipLimit !== undefined) {
this.clipLimit = options.clipLimit;
}
if (options.tileGridSize !== undefined) {
this.tileGridSize = options.tileGridSize;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix mismatch in tileGridSize property names in configure method

In the configure method, tileGridSize uses width and height, but the class property tileGridSize expects rows and columns. This inconsistency could lead to incorrect behavior when configuring the CLAHE parameters.

Apply this diff to align the property names:

public configure(options: {
    clipLimit?: number,
-   tileGridSize?: { width: number, height: number }
+   tileGridSize?: { rows: number, columns: number }
}): void {
    if (options.clipLimit !== undefined) {
        this.clipLimit = options.clipLimit;
    }
    if (options.tileGridSize !== undefined) {
-       this.tileGridSize = options.tileGridSize;
+       this.tileGridSize = options.tileGridSize;
    }
}

Alternatively, if you prefer to keep width and height, map them accordingly:

public configure(options: {
    clipLimit?: number,
    tileGridSize?: { width: number, height: number }
}): void {
    if (options.clipLimit !== undefined) {
        this.clipLimit = options.clipLimit;
    }
    if (options.tileGridSize !== undefined) {
        this.tileGridSize = {
+           rows: options.tileGridSize.height,
+           columns: options.tileGridSize.width,
        };
    }
}

Committable suggestion skipped: line range outside the PR's diff.

}

public processImage(src:ImageData, frameNumber: number) : ImageData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much of the code here is duplicated from histogram-equalization.ts. Could we create a base class to centralize the shared logic, particularly for the image processing functionality?

const { cv } = this;
let matImage = null;
const RGBImage = new cv.Mat();
const YUVImage = new cv.Mat();
const RGBDist = new cv.Mat();
const YUVDist = new cv.Mat();
const RGBADist = new cv.Mat();
let channels = new cv.MatVector();
const equalizedY = new cv.Mat();
const tileGridSize = new cv.Size(this.tileGridSize.rows, this.tileGridSize.columns);
const clahe = new cv.CLAHE(this.clipLimit, tileGridSize);
try {
this.currentProcessedImage = frameNumber;
matImage = cv.matFromImageData(src);
cv.cvtColor(matImage, RGBImage, cv.COLOR_RGBA2RGB, 0);
cv.cvtColor(RGBImage, YUVImage, cv.COLOR_RGB2YUV, 0);
cv.split(YUVImage, channels);
const [Y, U, V] = [channels.get(0), channels.get(1), channels.get(2)];
channels.delete();
channels = null;
clahe.apply(Y, equalizedY);
clahe.delete();
Y.delete();
channels = new cv.MatVector();
channels.push_back(equalizedY); equalizedY.delete();
channels.push_back(U); U.delete();
channels.push_back(V); V.delete();
cv.merge(channels, YUVDist);
cv.cvtColor(YUVDist, RGBDist, cv.COLOR_YUV2RGB, 0);
cv.cvtColor(RGBDist, RGBADist, cv.COLOR_RGB2RGBA, 0);
const arr = new Uint8ClampedArray(RGBADist.data, RGBADist.cols, RGBADist.rows);
const imgData = new ImageData(arr, src.width, src.height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the construction of Uint8ClampedArray

The Uint8ClampedArray constructor is incorrectly called with three arguments. It should be constructed directly from RGBADist.data.

Apply this fix:

- const arr = new Uint8ClampedArray(RGBADist.data, RGBADist.cols, RGBADist.rows);
+ const arr = new Uint8ClampedArray(RGBADist.data);

This ensures that the array correctly represents the pixel data for the ImageData object.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const arr = new Uint8ClampedArray(RGBADist.data, RGBADist.cols, RGBADist.rows);
const imgData = new ImageData(arr, src.width, src.height);
const arr = new Uint8ClampedArray(RGBADist.data);
const imgData = new ImageData(arr, src.width, src.height);

return imgData;
} catch (e) {
throw new Error(e.toString());
} finally {
if (matImage) matImage.delete();
if (channels) channels.delete();
RGBImage.delete();
YUVImage.delete();
RGBDist.delete();
YUVDist.delete();
RGBADist.delete();
}
}
}
3 changes: 3 additions & 0 deletions cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { ObjectState, ShapeType, getCore } from 'cvat-core-wrapper';
import waitFor from 'utils/wait-for';
import config from 'config';
import CLAHEImplementation, { CLAHE } from './clahe';
import HistogramEqualizationImplementation, { HistogramEqualization } from './histogram-equalization';
import TrackerMImplementation from './tracker-mil';
import IntelligentScissorsImplementation, { IntelligentScissors } from './intelligent-scissors';
Expand Down Expand Up @@ -33,6 +34,7 @@ export interface Contours {

export interface ImgProc {
hist: () => HistogramEqualization;
clahe: () => CLAHE;
}

export interface Tracking {
Expand Down Expand Up @@ -299,6 +301,7 @@ export class OpenCVWrapper {
this.checkInitialization();
return {
hist: () => new HistogramEqualizationImplementation(this.cv),
clahe: () => new CLAHEImplementation(this.cv),
};
}

Expand Down
19 changes: 17 additions & 2 deletions site/content/en/docs/manual/advanced/ai-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ See:
- [OpenCV: annotate with trackers](#opencv-annotate-with-trackers)
- [When tracking](#when-tracking)
- [Trackers models](#trackers-models)
- [OpenCV: histogram equalization](#opencv-histogram-equalization)
- [OpenCV](#opencv)
- [Histogram Equalization](#opencv-histogram-equalization)
- [Contrast Limited Adaptive Histogram Equalization](#opencv-clahe)
Comment on lines +34 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix anchor tags in TOC to match section IDs

The anchor tags in the TOC links don't match the actual section IDs. This will cause broken navigation.

Apply this diff to fix the anchor tags:

- - [OpenCV](#opencv)
-   - [Histogram Equalization](#opencv-histogram-equalization)
-   - [Contrast Limited Adaptive Histogram Equalization](#opencv-clahe)
+ - [OpenCV](#opencv-1)
+   - [Histogram Equalization](#histogram-equalization)
+   - [Contrast Limited Adaptive Histogram Equalization](#contrast-limited-adaptive-histogram-equalization)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [OpenCV](#opencv)
- [Histogram Equalization](#opencv-histogram-equalization)
- [Contrast Limited Adaptive Histogram Equalization](#opencv-clahe)
- [OpenCV](#opencv-1)
- [Histogram Equalization](#histogram-equalization)
- [Contrast Limited Adaptive Histogram Equalization](#contrast-limited-adaptive-histogram-equalization)


## Interactors

Expand Down Expand Up @@ -280,7 +282,8 @@ All annotated objects will be automatically tracked when you move to the next fr

<!--lint enable maximum-line-length-->

## OpenCV: histogram equalization
## OpenCV
### Histogram Equalization

**Histogram equalization** improves
the contrast by stretching the intensity range.
Expand All @@ -306,3 +309,15 @@ Example of the result:
![](/images/image222.jpg)

To disable **Histogram equalization**, click on the button again.

### Contrast Limited Adaptive Histogram Equalization
Contrast Limited Adaptive Histogram Equalization (CLAHE) increases contrast by applying clipped histogram equalization to multiple tiles across the input image. In images where there are both very bright and very dark regions, this improves contrast in the dark regions without loosing contrast in the bright ones.

#### Parameters
* Clip Limit: Maximum value a pixel can be adjusted. Higher values allow for noise to be over-amplified.
* Tile Rows: How many rows of tiles to break the image into.
* Tile Columns: How many columns of tiles to break the image into.


User Interface
![](/images/opencv-image-clahe_interaction.jpg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance CLAHE documentation for better user guidance

While the section provides a good overview, it needs several improvements to match the quality and format of the Histogram Equalization section:

  1. Missing step-by-step instructions for using CLAHE
  2. Parameter descriptions could be more detailed with recommended ranges
  3. Missing before/after example images to demonstrate the effect
  4. UI screenshot could use a caption or description

Consider applying these improvements:

### Contrast Limited Adaptive Histogram Equalization
Contrast Limited Adaptive Histogram Equalization (CLAHE) increases contrast by applying clipped histogram equalization to multiple tiles across the input image. In images where there are both very bright and very dark regions, this improves contrast in the dark regions without loosing contrast in the bright ones.

+ To apply CLAHE to your image:
+ 
+ 1. In the **OpenCV** menu, go to the **Image** tab
+ 2. Click on the **CLAHE** button
+ 3. Adjust the parameters as needed (see below)
+ 
#### Parameters
- * Clip Limit: Maximum value a pixel can be adjusted. Higher values allow for noise to be over-amplified.
- * Tile Rows: How many rows of tiles to break the image into.
- * Tile Columns: How many columns of tiles to break the image into.
+ * **Clip Limit** (Range: 1-10): Maximum value for contrast enhancement. 
+   - Lower values (1-3) prevent noise amplification but provide subtle enhancement
+   - Higher values (4-10) give stronger enhancement but may amplify noise
+ * **Tile Rows** (Range: 2-16): Number of rows to divide the image into.
+   - More rows allow for more localized enhancement
+   - Recommended: Start with 8 rows and adjust based on image size
+ * **Tile Columns** (Range: 2-16): Number of columns to divide the image into.
+   - More columns allow for more localized enhancement
+   - Recommended: Start with 8 columns and adjust based on image size

-User Interface
+#### User Interface
+The CLAHE interface provides sliders to adjust each parameter in real-time:
 ![](/images/opencv-image-clahe_interaction.jpg)

+#### Example Results
+Below is an example showing the effect of CLAHE:
+![Before and After CLAHE Example](/images/opencv-image-clahe-example.jpg)

Committable suggestion skipped: line range outside the PR's diff.

Binary file modified site/content/en/images/image221.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.