Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
923fb91
docs: streamline README by removing redundant sections and enhancing …
agfianf Jan 27, 2025
5cf011e
refactor: rename YOLOv8 related modules and update imports; enhance c…
agfianf Feb 1, 2025
e883a66
docs: update README usage section with improved example and additiona…
agfianf Feb 1, 2025
354d45f
ci: remove commented installation step from publish workflow
agfianf Feb 1, 2025
e0ebb7f
feat: implement color correction algorithms with linear, affine, poly…
agfianf Feb 1, 2025
d9a986d
chore: update .gitignore and add scikit-learn and joblib dependencies…
agfianf Feb 1, 2025
8904ff8
feat: enhance color correction model initialization with additional k…
agfianf Feb 2, 2025
d086053
feat: replace Ridge regression with Linear regression in polynomial c…
agfianf Feb 2, 2025
86c3f02
refactor: remove unused imports from linear regression module
agfianf Feb 2, 2025
a99c0b2
chore: add tmp/ directory to .gitignore
agfianf Feb 2, 2025
31e49c0
chore: update VSCode settings for Python testing and Git hooks config…
agfianf Feb 2, 2025
4c452d6
feat: add img_grid_patches_ref property to ColorCorrection class and …
agfianf Feb 2, 2025
e673f38
refactor: streamline preprocessing and postprocessing functions for g…
agfianf Feb 2, 2025
51526ff
refactor: streamline preprocessing and postprocessing functions for g…
agfianf Feb 3, 2025
53bc577
docs: update README with improved usage instructions for ColorCorrect…
agfianf Feb 3, 2025
d605666
docs: remove unnecessary import statement from usage example in README
agfianf Feb 3, 2025
89f33d0
feat: add main execution block for ColorCorrection class with example…
agfianf Feb 3, 2025
cc83e14
feat: implement CorrectionModelFactory and refactor visualization uti…
agfianf Feb 3, 2025
a4530e6
feat: enhance color patch processing and add tests for image patch ut…
agfianf Feb 3, 2025
cbb9c29
test: add unit tests for image processing utilities including croppin…
agfianf Feb 3, 2025
260f537
refactor: clean up import statements in YOLOv8 detector test file
agfianf Feb 3, 2025
d206c8a
chore: update version to 0.0.1b0 and add entry to changelog
agfianf Feb 3, 2025
03e0a9f
chore: update pytest arguments for more verbose test output
agfianf Feb 3, 2025
d28e6cc
chore: update changelog with improvements, features, documentation, d…
agfianf Feb 3, 2025
d458ca3
fix: correct modulo value in known image fixture for accurate test re…
agfianf Feb 3, 2025
882130e
chore: update color-correction-asdfghjkl version to 0.0.1b0 in uv.lock
agfianf Feb 3, 2025
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
3 changes: 0 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ jobs:
- name: Extract version from Git tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV

# - name: Install the project
# run: uv sync --all-groups --no-group dev-model

- name: Clear directory
run: rm -rf dist
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ wheels/
.pytest_cache/
.ruff_cache/

*.pyc
*.pyc

tmp/
zzz/
color_correction_asdfghjkl/asset/images/
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repos:
- id: trailing-whitespace
# python code formatting
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.2
rev: v0.9.4
hooks:
- id: ruff
types_or: [python, pyi, jupyter]
Expand Down
24 changes: 20 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
{
"ruff.lint.args": [
"--config=${workspaceFolder}/pyproject.toml"
"--config=${workspaceFolder}/pyproject.toml"
],
"ruff.format.args": [
"--config=${workspaceFolder}/pyproject.toml"
]
}
"--config=${workspaceFolder}/pyproject.toml"
],
"python.testing.pytestArgs": [
"tests -sv"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,

// Auto activate virtual environment
"python.terminal.activateEnvironment": true,
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",

// Configure terminal to open in workspace
"terminal.integrated.cwd": "${workspaceFolder}",

// Git settings for pre-commit
"git.postCommitCommand": "push",
"git.hooks.enable": true
}
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Changelog

## [v0.0.1b0] - 2025-02-03

### 🔧 Improvements
- **Color Correction Core**
- Added new correction models: polynomial, linear regression, and affine regression
- Improved patch detection and processing pipeline
- Added support for debug visualization outputs
- Enhanced color patch extraction with better error handling

### 🎨 Features
- **Reference Colors**
- Added RGB format reference colors alongside BGR
- Improved color patch visualization and comparison tools
- Added support for custom reference images

### 📝 Documentation
- **README Updates**
- Simplified usage documentation with clearer examples
- Added visual explanation of color correction workflow
- Updated installation and usage instructions

### 🛠️ Development
- **Project Structure**
- Reorganized core modules for better maintainability
- Added new utility modules for image processing
- Updated VSCode settings for better development experience

### 🔨 Build
- **Dependencies**
- Added scikit-learn for advanced correction models
- Updated ruff to v0.9.4
- Added pre-commit hooks configuration

### 🧪 Testing
- **Test Coverage**
- Added new test cases for image processing utilities
- Improved test organization and structure



## [v0.0.1a2] - 2025-01-27

### 🚀 New Features
Expand Down Expand Up @@ -51,4 +91,4 @@

### 📝 Initial Setup
- **Initialize project with Python version, .gitignore, VSCode settings, pre-commit configuration, and pyproject.toml** (71a8c74)
- **Add README.md for Color Correction package documentation** (2b35650)
- **Add README.md for Color Correction package documentation** (2b35650)
65 changes: 28 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,10 @@

# 🎨 Color Correction Toolkit
# 🎨 Color Correction

> **Note:** The "asdfghjkl" is just a placeholder due to some naming difficulties.

This package is designed to perform color correction on images using the Color Checker Classic 24 Patch card. It provides a robust solution for ensuring accurate color representation in your images.

## 🛠️ How It Works

1. **Input Requirements**:
- An image containing the Color Checker Classic 24 Patch card (referred to as the color checker).
- A reference image of the color checker.

2. **Process**:
- The package computes a color correction matrix based on the input image and reference image.
- This matrix can then be used to correct the colors of the input image or other images with the same color profile.

**Workflow**:
```
Input Image + Reference Image -> Color Correction Matrix -> Corrected Image
```

3. **Advanced Usage**:
- For multiple images, it’s recommended to preprocess the images using white balance before calculating the color correction matrix. This ensures that all images are in the same color profile, leading to a more accurate correction matrix.

**Enhanced Workflow**:
```
Input Images -> White Balance -> Compute (Input Image with Color Checker, Reference Image) -> Color Correction Matrix -> Corrected Images
```

## 📈 Benefits
- **Consistency**: Ensure uniform color correction across multiple images.
- **Accuracy**: Leverage the color correction matrix for precise color adjustments.
- **Flexibility**: Adaptable for various image sets with different color profiles.

## Installation

```bash
Expand All @@ -41,24 +13,42 @@ pip install color-correction-asdfghjkl
## Usage

```python
import cv2
# Step 1: Define the path to the input image
image_path = "asset/images/cc-19.png"

from color_correction_asdfghjkl import ColorCorrection
# Step 2: Load the input image
input_image = cv2.imread(image_path)

cc = ColorCorrection(
# Step 3: Initialize the color correction model with specified parameters
color_corrector = ColorCorrection(
detection_model="yolov8",
detection_conf_th=0.25,
correction_model="least_squares",
use_gpu=False,
degree=2, # for polynomial correction model
use_gpu=True,
)

# Step 4: Extract color patches from the input image
color_corrector.set_input_patches(image=input_image, debug=True)
color_corrector.fit()
corrected_image = color_corrector.predict(
input_image=input_image,
debug=True,
debug_output_dir="zzz",
)

input_image = cv2.imread("cc-19.png")
cc.fit(input_image=input_image)
corrected_image = cc.correct_image(input_image=input_image)
cv2.imwrite("corrected_image.png", corrected_image)
```
Sample output:
![Sample Output](assets/sample-output-usage.png)

## 📈 Benefits
- **Consistency**: Ensure uniform color correction across multiple images.
- **Accuracy**: Leverage the color correction matrix for precise color adjustments.
- **Flexibility**: Adaptable for various image sets with different color profiles.

![How it works](assets/color-correction-how-it-works.png)


<!-- write reference -->
## 📚 References
- [Color Checker Classic 24 Patch Card](https://www.xrite.com/categories/calibration-profiling/colorchecker-classic)
Expand All @@ -68,3 +58,4 @@ Sample output:
- [Automatic color correction with OpenCV and Python (PyImageSearch)](https://pyimagesearch.com/2021/02/15/automatic-color-correction-with-opencv-and-python/)
- [ONNX-YOLOv8-Object-Detection](https://github.com/ibaiGorordo/ONNX-YOLOv8-Object-Detection)
- [yolov8-triton](https://github.com/omarabid59/yolov8-triton/tree/main)
- [Streamlined Data Science Development: Organizing, Developing and Documenting Your Code](https://medium.com/henkel-data-and-analytics/streamlined-data-science-development-organizing-developing-and-documenting-your-code-bfd69e3ef4fb)
Binary file added assets/color-correction-how-it-works.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions color_correction_asdfghjkl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
__version__ = "0.0.1a1"

# fmt: off
from .constant.color_checker import reference_color_d50 as REFERENCE_COLOR_D50 # noqa: N812, I001
from .core.card_detection.yolov8_det_onnx import YOLOv8CardDetector
from .schemas.yolov8_det import DetectionResult as YOLOv8DetectionResult
from .constant.color_checker import reference_color_d50_bgr as REFERENCE_COLOR_D50_BGR # noqa: N812, I001
from .constant.color_checker import reference_color_d50_rgb as REFERENCE_COLOR_D50_RGB # noqa: N812, I001
from .core.card_detection.det_yv8_onnx import YOLOv8CardDetector
from .schemas.det_yv8 import DetectionResult as YOLOv8DetectionResult
from .services.color_correction import ColorCorrection

# fmt: on

__all__ = [
"__version__",
"REFERENCE_COLOR_D50",
"REFERENCE_COLOR_D50_BGR",
"REFERENCE_COLOR_D50_RGB",
"ColorCorrection",
"YOLOv8CardDetector",
"YOLOv8DetectionResult",
Expand Down
31 changes: 30 additions & 1 deletion color_correction_asdfghjkl/constant/color_checker.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import numpy as np

# in BGR format
reference_color_d50 = np.array(
reference_color_d50_bgr = np.array(
[
[68, 82, 115], # 1. Dark skin
[128, 149, 195], # 2. Light skin
Expand Down Expand Up @@ -29,3 +29,32 @@
[51, 50, 50], # 24. Black 2
],
)

reference_color_d50_rgb = np.array(
[
[115, 82, 68], # 1. Dark skin
[195, 149, 128], # 2. Light skin
[93, 123, 157], # 3. Blue sky
[91, 108, 65], # 4. Foliage
[130, 129, 175], # 5. Blue flower
[99, 191, 171], # 6. Bluish green
[220, 123, 46], # 7. Orange
[72, 92, 168], # 8. Purplish blue
[194, 84, 97], # 9. Moderate red
[91, 59, 104], # 10. Purple
[161, 189, 62], # 11. Yellow green
[229, 161, 40], # 12. Orange yellow
[42, 63, 147], # 13. Blue
[72, 149, 72], # 14. Green
[175, 50, 57], # 15. Red
[238, 200, 22], # 16. Yellow
[188, 84, 150], # 17. Magenta
[0, 137, 166], # 18. Cyan
[245, 245, 240], # 19. White 9.5
[201, 202, 201], # 20. Neutral 8
[161, 162, 162], # 21. Neutral 6.5
[120, 121, 121], # 22. Neutral 5
[83, 85, 85], # 23. Neutral 3.5
[50, 50, 51], # 24. Black 2
],
)
2 changes: 1 addition & 1 deletion color_correction_asdfghjkl/core/card_detection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

from color_correction_asdfghjkl.schemas.yolov8_det import DetectionResult
from color_correction_asdfghjkl.schemas.det_yv8 import DetectionResult


class BaseCardDetector(ABC):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import onnxruntime

from color_correction_asdfghjkl.core.card_detection.base import BaseCardDetector
from color_correction_asdfghjkl.schemas.yolov8_det import DetectionResult
from color_correction_asdfghjkl.schemas.det_yv8 import DetectionResult
from color_correction_asdfghjkl.utils.downloader import downloader_model_yolov8
from color_correction_asdfghjkl.utils.yolo_utils import (
multiclass_nms,
Expand Down Expand Up @@ -34,7 +34,6 @@ def __init__(
self.iou_threshold = iou_th
self.use_gpu = use_gpu
if path is None:
print("Auto downloading YOLOv8 model...")
path = downloader_model_yolov8(use_gpu)
self.__initialize_model(path)

Expand All @@ -53,7 +52,7 @@ def detect(self, image: np.ndarray) -> DetectionResult:
A dataclass containing detected bounding boxes, confidence scores,
and class IDs.
"""
input_tensor = self.__prepare_input(image)
input_tensor = self.__prepare_input(image.copy())
outputs = self.__inference(input_tensor)
boxes, scores, class_ids = self.__process_output(outputs)

Expand Down
17 changes: 17 additions & 0 deletions color_correction_asdfghjkl/core/correction/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from color_correction_asdfghjkl.core.correction._factory import (
CorrectionModelFactory,
)
from color_correction_asdfghjkl.core.correction.affine_reg import AffineReg
from color_correction_asdfghjkl.core.correction.least_squares import (
LeastSquaresRegression,
)
from color_correction_asdfghjkl.core.correction.linear_reg import LinearReg
from color_correction_asdfghjkl.core.correction.polynomial import Polynomial

__all__ = [
"CorrectionModelFactory",
"LeastSquaresRegression",
"Polynomial",
"LinearReg",
"AffineReg",
]
18 changes: 18 additions & 0 deletions color_correction_asdfghjkl/core/correction/_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from color_correction_asdfghjkl.core.correction.affine_reg import AffineReg
from color_correction_asdfghjkl.core.correction.least_squares import (
LeastSquaresRegression,
)
from color_correction_asdfghjkl.core.correction.linear_reg import LinearReg
from color_correction_asdfghjkl.core.correction.polynomial import Polynomial


class CorrectionModelFactory:
@staticmethod
def create(model_name: str, **kwargs: dict) -> ...:
model_registry = {
"least_squares": LeastSquaresRegression(),
"polynomial": Polynomial(**kwargs),
"linear_reg": LinearReg(),
"affine_reg": AffineReg(),
}
return model_registry.get(model_name)
41 changes: 41 additions & 0 deletions color_correction_asdfghjkl/core/correction/affine_reg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import time

import numpy as np
from sklearn.linear_model import LinearRegression

from color_correction_asdfghjkl.core.correction.base import BaseComputeCorrection
from color_correction_asdfghjkl.utils.correction import (
postprocessing_compute,
preprocessing_compute,
)


class AffineReg(BaseComputeCorrection):
def __init__(self) -> None:
self.model = None

def fit(
self,
x_patches: np.ndarray, # input patches
y_patches: np.ndarray, # reference patches
) -> np.ndarray:
start_time = time.perf_counter()
x_patches = np.array(x_patches)
print("x_patches.shape", x_patches.shape)
x_patches = np.hstack([x_patches, np.ones((x_patches.shape[0], 1))])
self.model = LinearRegression(fit_intercept=False).fit(x_patches, y_patches)

exc_time = time.perf_counter() - start_time
print(f"{self.__class__.__name__} Fit: {exc_time} seconds")
return self.model

def compute_correction(self, input_image: np.ndarray) -> np.ndarray:
if self.model is None:
raise ValueError("Model is not fitted yet. Please call fit() method first.")

org_input_shape = input_image.shape
input_image = preprocessing_compute(input_image)
input_image = np.hstack([input_image, np.ones((input_image.shape[0], 1))])
image = self.model.predict(input_image)
corrected_image = postprocessing_compute(org_input_shape, image)
return corrected_image
Loading