Init Commit

This commit is contained in:
ldy 2025-06-09 16:47:55 +08:00
parent 16c991b3dd
commit 7b47850f9e
19 changed files with 45013 additions and 1 deletions

162
.gitignore vendored Normal file
View File

@ -0,0 +1,162 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.dataset/

View File

@ -1,2 +1,22 @@
# ImageOptimizer # Image Optimizer
UIC DPW 24S **Group 11** course project
## Set Up
1. **Install packages using *requirements.txt*:** `pip install -r requirements.txt`.
2. **Ensure `.\icon` folder is correctly placed** in the same directory with the python files.
3. The prepared JSON filter datasets are placed in `.\dataset` folder. Although the program can run without these dataset files, you may not be able to observe the dataset and do image auto optimization as it requires dataset for analysis and training.
4. **Run** `main.py`.
## Usage: Image Optimization
1. **Adjust Atomic Parameter of an Image:** Click in and change the value in a text box located at the left-bottom of `Main` frame. Then click out of the canvas or simply click the *Image histogram* on the top to modify the image & refresh the window.
2. **Auto Optimization:** Choose a filter in the combo box located at the right-bottom of the `Main` frame, and click `Auto Optimizzation` button.
3. **Export Image:** Simple click `Export` button, then a new window will appear. You may crop, compress, and choose clarity of the output image.
4. **Revert Image:** We offer `Revert To Original` button to restart the editing process.
## Usage: Dataset Management
1. **New Dataset Creation:** Simply Create a new folder in `.\dataset` and push the `Update Dataset` button on the `Dataset Manamgement` frame.
2. **Dataset Update:** Add new images into the corresponding filter folder in`.\dataset` and push the `Update Dataset` button on the `Dataset Manamgement` frame.
3. **Update Dataset By Unsplash Crawler:**
Open the `Dataset Management` frame, click `Get More Images from Unsplash`.
Specify which filter datasets you want to update, each inner API can only crawl *500 images/hour*.
After crawling, click `Dataset Update` button.

379
analyze_func.py Normal file
View File

@ -0,0 +1,379 @@
import os
import json
import cv2 as cv
import numpy as np
import pandas as pd
# Normalization: Scale 0-100
def normalize_value(value, min_value, max_value):
# Ensure the values are within the correct range
if value < min_value:
value = min_value
elif value > max_value:
value = max_value
# Normalize the value to a 0-100 scale
normalized_value = 100.0 * (value - min_value) / (max_value - min_value)
return normalized_value
def denormalize_value(normalized_value, min_value, max_value):
denormalized_value = (normalized_value / 100.0) * (max_value - min_value) + min_value
return denormalized_value
# Get & Modify Functions
def get_noise(img):
grayscale_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
noise_level = np.std(grayscale_image)
normalized_noise = normalize_value(noise_level, 0, 255)
return normalized_noise
def modify_noise(img, noise_factor):
noise_factor_denormalized = denormalize_value(noise_factor, 0, 15)
gaussian_noise = np.random.normal(0, noise_factor_denormalized, img.shape)
noisy_img = np.clip(img + gaussian_noise, 0, 255).astype(np.uint8)
return noisy_img
def get_hue(img):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hue_values = hsv_image[:, :, 0]
average_hue = np.mean(hue_values)
normalized_hue = normalize_value(average_hue, 0, 179)
return normalized_hue
def modify_hue(img, hue_factor):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
hue_values = hsv_image[:, :, 0]
target_hue = denormalize_value(hue_factor, 0, 179)
hue_adjustment = target_hue - np.mean(hue_values)
# Ensure hue_values and hue_adjustment are numpy arrays
if isinstance(hue_values, np.ndarray) and isinstance(hue_adjustment, (int, float, np.ndarray)):
hsv_image[:, :, 0] = (hue_values + hue_adjustment) % 180
modified_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return modified_img
def get_brightness(img):
average_brightness = np.mean(img)
normalized_brightness = normalize_value(average_brightness, 0, 255)
return normalized_brightness
def modify_brightness(img, brightness_factor):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
current_avg_brightness = np.mean(hsv_image[:, :, 2])
target_brightness = denormalize_value(brightness_factor, 0, 255)
if current_avg_brightness == 0:
brightness_adjustment = 1
else:
brightness_adjustment = target_brightness / current_avg_brightness
hsv_image[:, :, 2] = np.clip(hsv_image[:, :, 2] * brightness_adjustment, 0, 255).astype(np.uint8)
modified_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return modified_img
def get_perceived_avg_brightness(img):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
value_values = hsv_image[:, :, 2]
average_value = np.mean(value_values)
normalized_value = normalize_value(average_value, 0, 255)
return normalized_value
def modify_perceived_avg_brightness(img, target_avg_value):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
value_values = hsv_image[:, :, 2]
current_avg_value = np.mean(value_values)
target_brightness = denormalize_value(target_avg_value, 0, 255)
if current_avg_value == 0:
brightness_adjustment = 1
else:
brightness_adjustment = target_brightness / current_avg_value
adjusted_value = np.clip(value_values * brightness_adjustment, 0, 255)
hsv_image[:, :, 2] = adjusted_value.astype(np.uint8)
adjusted_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return adjusted_img
def get_white_balance(img):
# Calculate mean values for each color channel
mean_red = np.mean(img[:, :, 2])
mean_green = np.mean(img[:, :, 1])
mean_blue = np.mean(img[:, :, 0])
# Calculate mean intensity of the grayscale image
mean_gray = np.mean(cv.cvtColor(img, cv.COLOR_BGR2GRAY))
# Avoid division by zero
if mean_red == 0 or mean_green == 0 or mean_blue == 0:
return 0, 0, 0
# Calculate balance ratios for each channel
balance_ratio_red = mean_gray / mean_red
balance_ratio_green = mean_gray / mean_green
balance_ratio_blue = mean_gray / mean_blue
# Normalize each ratio separately
normalized_red = normalize_value(balance_ratio_red, 0, 2)
normalized_green = normalize_value(balance_ratio_green, 0, 2)
normalized_blue = normalize_value(balance_ratio_blue, 0, 2)
return normalized_red, normalized_green, normalized_blue
def modify_white_balance(image, balance_ratio_red=-1, balance_ratio_green=-1, balance_ratio_blue=-1):
image = image.astype(np.float32)
b, g, r = cv.split(image)
# If balance ratios are given as normalized values (0-100), convert them to appropriate ratios
if balance_ratio_red != -1:
balance_ratio_red = denormalize_value(balance_ratio_red, 0, 2)
r = r * balance_ratio_red
if balance_ratio_green != -1:
balance_ratio_green = denormalize_value(balance_ratio_green, 0, 2)
g = g * balance_ratio_green
if balance_ratio_blue != -1:
balance_ratio_blue = denormalize_value(balance_ratio_blue, 0, 2)
b = b * balance_ratio_blue
r = np.clip(r, 0, 255)
g = np.clip(g, 0, 255)
b = np.clip(b, 0, 255)
balanced_image = cv.merge((b, g, r))
balanced_image = balanced_image.astype(np.uint8)
return balanced_image
def get_color_temperature(img):
# Calculate the mean values for each color channel
mean_red = np.mean(img[:, :, 0])
mean_green = np.mean(img[:, :, 1])
mean_blue = np.mean(img[:, :, 2])
# Calculate color temperature based on the ratio of blue to red
color_temperature = (mean_blue - mean_red) / (mean_blue + mean_green + mean_red)
normalized_color_temperature = normalize_value(color_temperature, -100, 100)
return normalized_color_temperature
def modify_color_temperature(img, temperature_factor):
modified_img = img.copy().astype(np.float32)
temperature_factor = denormalize_value(temperature_factor, -100, 100)
# Adjust color temperature by scaling blue and red channels
modified_img[:, :, 0] += temperature_factor
modified_img[:, :, 2] -= temperature_factor
# Clip values to ensure they remain in valid range
modified_img = np.clip(modified_img, 0, 255)
return modified_img.astype(np.uint8)
def get_contrast(img):
grayscale_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
contrast = np.std(grayscale_image)
# Normalize to the range 0-100
normalized_contrast = normalize_value(contrast, 0, 128)
return normalized_contrast
def modify_contrast(img, contrast_factor):
grayscale_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
mean_intensity = np.mean(grayscale_image)
contrast_target = denormalize_value(contrast_factor, 0, 128)
contrast_adjustment = (2 * contrast_target / 128) - 1
# Adjust contrast for each channel separately
adjusted_channels = []
for i in range(3):
adjusted_channel = np.clip((img[:, :, i] - mean_intensity) * (1 + contrast_adjustment) + mean_intensity, 0,
255).astype(np.uint8)
adjusted_channels.append(adjusted_channel)
# Merge the adjusted channels back into a BGR image
modified_img = cv.merge((adjusted_channels[0], adjusted_channels[1], adjusted_channels[2]))
return modified_img
def get_saturation(img):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
saturation_values = hsv_image[:, :, 1]
average_saturation = np.mean(saturation_values)
normalized_saturation = normalize_value(average_saturation, 0, 255)
return normalized_saturation
def modify_saturation(img, saturation_factor):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
saturation_values = hsv_image[:, :, 1]
current_avg_saturation = np.mean(saturation_values)
target_saturation = denormalize_value(saturation_factor, 0, 255)
if current_avg_saturation == 0:
saturation_adjustment = 1
else:
saturation_adjustment = target_saturation / current_avg_saturation
hsv_image[:, :, 1] = np.clip(saturation_values * saturation_adjustment, 0, 255).astype(np.uint8)
modified_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return modified_img
def get_sharpness(img):
grayscale_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
laplacian_var = cv.Laplacian(grayscale_image, cv.CV_64F).var()
# Adjusted max value for Laplacian variance based on typical image sharpness
normalized_sharpness = normalize_value(laplacian_var, 0, 1000)
return normalized_sharpness
def modify_sharpness(img, sharpen_factor):
if sharpen_factor < 0:
raise ValueError("sharpen_factor must be non-negative")
sharpness_adjustment = denormalize_value(sharpen_factor, 0, 1000)
sharpness_adjustment = (5 * sharpness_adjustment / 1000) - 1
blurred_img = cv.GaussianBlur(img, (0, 0), 3)
sharp_img = cv.addWeighted(img, 1 + sharpness_adjustment, blurred_img, -sharpness_adjustment, 0)
return sharp_img
def get_highlights(img):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
highlights = hsv_image[:, :, 2].astype(np.float32)
highlighted_pixels = highlights[highlights > 200]
if highlighted_pixels.size == 0:
return 0
average_highlights = np.mean(highlighted_pixels)
normalized_highlights = normalize_value(average_highlights, 200, 255)
return normalized_highlights
def modify_highlights(img, highlights_factor):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
value_channel = hsv_image[:, :, 2]
current_avg_highlights = np.mean(value_channel)
target_highlights = denormalize_value(highlights_factor, 25, 255)
if current_avg_highlights == 0:
highlights_adjustment = 1
else:
highlights_adjustment = target_highlights / current_avg_highlights
value_channel = np.clip(value_channel * highlights_adjustment, 0, 225)
hsv_image[:, :, 2] = value_channel.astype(np.uint8)
modified_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return modified_img
def get_shadows(img):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
shadows = hsv_image[:, :, 2].astype(np.float32)
shadowed_pixels = shadows[shadows < 50]
if shadowed_pixels.size == 0:
return 0
average_shadows = np.mean(shadowed_pixels)
normalized_shadows = normalize_value(average_shadows, 0, 50)
return normalized_shadows
def modify_shadows(img, shadows_factor):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
value_channel = hsv_image[:, :, 2]
current_avg_shadows = np.mean(value_channel)
target_shadows = denormalize_value(shadows_factor, 0, 225)
if current_avg_shadows == 0:
shadows_adjustment = 1
else:
shadows_adjustment = target_shadows / current_avg_shadows
value_channel = np.clip(value_channel * shadows_adjustment, 0, 255)
hsv_image[:, :, 2] = value_channel.astype(np.uint8)
modified_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return modified_img
def get_exposure(img):
grayscale_image = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
average_exposure = np.mean(grayscale_image)
normalized_exposure = normalize_value(average_exposure, 0, 255)
return normalized_exposure
def modify_exposure(img, exposure_factor):
hsv_image = cv.cvtColor(img, cv.COLOR_BGR2HSV)
value_channel = hsv_image[:, :, 2]
current_avg_exposure = np.mean(value_channel)
target_exposure = denormalize_value(exposure_factor, 0, 255)
if current_avg_exposure == 0:
exposure_adjustment = 1
else:
exposure_adjustment = target_exposure / current_avg_exposure
value_channel = np.clip(value_channel * exposure_adjustment, 0, 255)
hsv_image[:, :, 2] = value_channel.astype(np.uint8)
modified_img = cv.cvtColor(hsv_image, cv.COLOR_HSV2BGR)
return modified_img
# Image Full-Analyze Function
def image_analysis(img_path, res_path):
src = cv.imread(img_path)
# Get image parameter values
WB_red, WB_green, WB_blue = get_white_balance(src)
average_brightness = get_brightness(src)
contrast = get_contrast(src)
average_hue = get_hue(src)
average_saturation = get_saturation(src)
average_perceived_brightness = get_perceived_avg_brightness(src)
average_sharpen = get_sharpness(src)
average_highlights = get_highlights(src)
average_shadow = get_shadows(src)
average_temperature = get_color_temperature(src)
average_noisy = get_noise(src)
average_exposure = get_exposure(src)
# Store img parameters into a Pandas DataFrame
results_df = pd.DataFrame({
"File": [img_path],
"contrast": [contrast],
"WB_red": [WB_red],
"WB_green": [WB_green],
"WB_blue": [WB_blue],
"avg_brightness": [average_brightness],
"avg_perceived_brightness": [average_perceived_brightness],
"avg_hue": [average_hue],
"avg_saturation": [average_saturation],
"avg_sharpness": [average_sharpen],
"avg_highlights": [average_highlights],
"avg_shadow": [average_shadow],
"avg_temperature": [average_temperature],
"avg_noisy": [average_noisy],
"avg_exposure": [average_exposure]
})
# Convert DataFrame to JSON and save to file
results_dict = results_df.to_dict(orient='records')[0]
results_json = json.dumps(results_dict, indent=4)
with open(res_path, 'a') as f:
f.write(results_json)
f.write('\n')
# Successfully executed notification
print("Image " + img_path + " parameters have been extracted to " + res_path)
# Dataset Update Function
def process_images_in_folders(root_dir):
for sub_folder in os.listdir(root_dir):
sub_folder_path = os.path.join(root_dir, sub_folder)
if os.path.isdir(sub_folder_path):
# Define the output JSON file path
output_file = os.path.join(sub_folder_path, f"{sub_folder}_result.json")
if not os.path.exists(output_file):
with open(output_file, 'w'):
pass
# Gather all image file paths in the sub-folder
image_paths = [os.path.join(sub_folder_path, f) for f in os.listdir(sub_folder_path) if
os.path.isfile(os.path.join(sub_folder_path, f)) and f != f"{sub_folder}_result.json"]
if not image_paths:
print(f"Dataset {sub_folder} is up to date.")
else:
# Perform image analysis and save the results
for image_path in image_paths:
image_analysis(image_path, output_file)
os.remove(image_path)
print(f"Processed {sub_folder} and saved results to {output_file}")
if __name__ == "__main__":
# Define the root dataset directory
dataset_dir = ".\dataset"
# Process the images in each sub-folder
process_images_in_folders(dataset_dir)

198
crawler.py Normal file
View File

@ -0,0 +1,198 @@
import os
import requests
import time
import customtkinter as ctk
from tkinter import messagebox, StringVar
# Unsplash API
api_url = "https://api.unsplash.com/search/photos"
default_access_key = ['-7mzLi8x261zMRKU-Rw_Qz_vIvaYvf2sWHV1l1pO5ZE',
'y0TRzwdfxA4PpZBPw3Lr8a6bYyAugiKItS7b_80UTA4']
# Crawl parameter
categories = {
'animals': 'animals',
'people': 'people',
'food-drink': 'food drink',
'nature': 'nature',
'architecture-interior': 'architecture interior'
}
# Save directory and changelog file
os.makedirs('dataset', exist_ok=True)
changelog_path = os.path.join('dataset', 'changelog.txt')
# Initialize page and images_downloaded
category_state = {category: {'page': 1, 'images_downloaded': 0} for category in categories}
# Check if changelog file exists and read the state if it does
if os.path.exists(changelog_path):
with open(changelog_path, 'r') as changelog_file:
for line in changelog_file:
category, page, images_downloaded = line.strip().split(',')
category_state[category]['page'] = int(page)
category_state[category]['images_downloaded'] = int(images_downloaded)
# Create changelog file
else:
with open(changelog_path, 'w') as changelog_file:
for category in categories:
changelog_file.write(f'{category},1,0\n')
# Create directories for categories if they don't exist
for category in categories:
os.makedirs(os.path.join('dataset', category), exist_ok=True)
# Crawling
def crawl_images(category, query, download_num, access_key):
if download_num == 0:
print("No need to update: " + category)
return
global category_state
img_index = category_state[category]['images_downloaded']
cur_page = category_state[category]['page']
flag = 0
if img_index % 10 != 0:
cur_page -= 1
real_img_index = int((cur_page - 1) * 10)
try:
while download_num > 0:
params = {
'query': query,
'client_id': access_key,
'per_page': 50,
'page': cur_page
}
response = requests.get(api_url, params=params)
if response.status_code == 200:
data = response.json()
images = data['results']
print(f'Found {len(images)} images for category {category} on page {cur_page}')
for idx, img in enumerate(images):
if download_num <= 0:
break
img_url = img['urls']['regular']
# Ignore crawled images
if flag == 0 and img_index > real_img_index:
real_img_index += 1
print(img_index)
print(real_img_index)
continue
else:
flag = 1
# Crawls and Store images
try:
img_data = requests.get(img_url).content
img_filename = os.path.join('dataset', category, f'{category}_{img_index + 1}.jpg')
with open(img_filename, 'wb') as handler:
handler.write(img_data)
print(f'Downloaded {img_filename}')
img_index += 1
download_num -= 1
except Exception as e:
messagebox.showerror("Download Error", f'Failed to download {img_url}: {e}')
break
cur_page += 1
time.sleep(1)
else:
messagebox.showerror("Download Error",
f'Failed to fetch images for category {category}: {response.status_code}. '
f'It can be caused by the API limit. Please try again after 1hr.')
break
finally:
# Update changelog file after finishing each category
category_state[category]['page'] = cur_page
category_state[category]['images_downloaded'] = img_index
with open(changelog_path, 'w') as new_changelog_file:
for cat in categories:
new_changelog_file.write(
f'{cat},{category_state[cat]["page"]},{category_state[cat]["images_downloaded"]}\n')
print("Completed: " + category)
def start_crawling():
start_button.configure(text="Processing", fg_color="red")
start_button.update()
access_key = access_key_combo.get()
try:
total_images_to_download = {
'animals': int(animals_var.get()),
'people': int(people_var.get()),
'food-drink': int(food_drink_var.get()),
'nature': int(nature_var.get()),
'architecture-interior': int(architecture_interior_var.get())
}
# Validate input values
total_count = sum(total_images_to_download.values())
if total_count <= 0:
messagebox.showerror("Invalid input", "Total number of images to download must be greater than 0.")
return
if total_count > 500:
messagebox.showerror("Invalid input", "The total number of images to download must not exceed 500.")
return
# Start Crawling
for curr_category, curr_query in categories.items():
print(curr_category)
crawl_images(curr_category, curr_query, total_images_to_download[curr_category], access_key)
except ValueError:
messagebox.showerror("Invalid input", "Please enter valid integer values.")
return
finally:
start_button.configure(text="Start Crawling", fg_color="#3b8ed0")
start_button.update()
if __name__ == "__main__":
# Create the main window
root = ctk.CTk()
root.title("Image Crawler")
root.iconbitmap('./icon/icon.ico')
# Create StringVar instances for each entry
animals_var = StringVar(value="0")
people_var = StringVar(value="0")
food_drink_var = StringVar(value="0")
nature_var = StringVar(value="0")
architecture_interior_var = StringVar(value="0")
# Create and place widgets
ctk.CTkLabel(root, text="Unsplash API Access Key:").grid(row=0, column=0, padx=10, pady=5)
access_key_combo = ctk.CTkComboBox(root, values=default_access_key, width=200)
access_key_combo.grid(row=0, column=1, padx=10, pady=5)
ctk.CTkLabel(root, text="Animals:").grid(row=1, column=0, padx=10, pady=5)
animals_entry = ctk.CTkEntry(root, textvariable=animals_var, width=100)
animals_entry.grid(row=1, column=1, padx=10, pady=5)
ctk.CTkLabel(root, text="People:").grid(row=2, column=0, padx=10, pady=5)
people_entry = ctk.CTkEntry(root, textvariable=people_var, width=100)
people_entry.grid(row=2, column=1, padx=10, pady=5)
ctk.CTkLabel(root, text="Food & Drink:").grid(row=3, column=0, padx=10, pady=5)
food_drink_entry = ctk.CTkEntry(root, textvariable=food_drink_var, width=100)
food_drink_entry.grid(row=3, column=1, padx=10, pady=5)
ctk.CTkLabel(root, text="Nature:").grid(row=4, column=0, padx=10, pady=5)
nature_entry = ctk.CTkEntry(root, textvariable=nature_var, width=100)
nature_entry.grid(row=4, column=1, padx=10, pady=5)
ctk.CTkLabel(root, text="Architecture & Interior:").grid(row=5, column=0, padx=10, pady=5)
architecture_interior_entry = ctk.CTkEntry(root, textvariable=architecture_interior_var, width=100)
architecture_interior_entry.grid(row=5, column=1, padx=10, pady=5)
start_button = ctk.CTkButton(root, text="Start Crawling", command=start_crawling)
start_button.grid(row=6, column=0, columnspan=2, padx=10, pady=20)
# Start Window
root.mainloop()

View File

@ -0,0 +1,204 @@
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_1.jpg",
"contrast": 55.64813561220745,
"WB_red": 48.79154254513347,
"WB_green": 49.67338099850525,
"WB_blue": 55.445994623748476,
"avg_brightness": 50.97080509158396,
"avg_perceived_brightness": 54.94651189179376,
"avg_hue": 21.001697315332095,
"avg_saturation": 21.201111262406197,
"avg_sharpness": 76.98735656410744,
"avg_highlights": 48.31948020241477,
"avg_shadow": 62.753570556640625,
"avg_temperature": 50.0209656113776,
"avg_noisy": 27.933181797500207,
"avg_exposure": 52.13299745824255
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_131.jpg",
"contrast": 25.575424890091362,
"WB_red": 49.63131523072817,
"WB_green": 50.173830506769654,
"WB_blue": 50.07804686065642,
"avg_brightness": 72.4241426611797,
"avg_perceived_brightness": 76.07639141047365,
"avg_hue": 51.81023188380808,
"avg_saturation": 9.948864278221578,
"avg_sharpness": 14.309853830366915,
"avg_highlights": 38.10055819424716,
"avg_shadow": 67.8313217163086,
"avg_temperature": 50.001496629194015,
"avg_noisy": 12.837860336987037,
"avg_exposure": 72.36611948277253
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_224.jpg",
"contrast": 39.1498678028228,
"WB_red": 47.29628466204506,
"WB_green": 48.95383763655193,
"WB_blue": 67.38785796866736,
"avg_brightness": 22.083168963363427,
"avg_perceived_brightness": 25.93154401678367,
"avg_hue": 14.333392085788802,
"avg_saturation": 40.62220787720666,
"avg_sharpness": 40.72319300374671,
"avg_highlights": 36.70202081853693,
"avg_shadow": 45.966304779052734,
"avg_temperature": 50.055875026241594,
"avg_noisy": 19.6516983480836,
"avg_exposure": 23.488490814707227
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_230.jpg",
"contrast": 54.56607631557097,
"WB_red": 46.28730494004484,
"WB_green": 50.419968991233524,
"WB_blue": 60.01410652544925,
"avg_brightness": 33.361521199362684,
"avg_perceived_brightness": 39.675422119333504,
"avg_hue": 25.318248680358046,
"avg_saturation": 44.76403236739782,
"avg_sharpness": 5.690056793933645,
"avg_highlights": 47.28293678977273,
"avg_shadow": 46.007930755615234,
"avg_temperature": 50.042525097995146,
"avg_noisy": 27.390030464286603,
"avg_exposure": 34.45230614808433
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_359.jpg",
"contrast": 35.961782934957434,
"WB_red": 47.56014736554645,
"WB_green": 50.310628718141814,
"WB_blue": 55.69945451520712,
"avg_brightness": 66.65015263993114,
"avg_perceived_brightness": 71.47217279916082,
"avg_hue": 12.380429253741637,
"avg_saturation": 16.6490866819979,
"avg_sharpness": 11.862548218358471,
"avg_highlights": 16.92868319424716,
"avg_shadow": 58.21811294555664,
"avg_temperature": 50.02610191778624,
"avg_noisy": 18.051404767351183,
"avg_exposure": 67.94562656338255
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_360.jpg",
"contrast": 55.386977042781304,
"WB_red": 48.705522563381415,
"WB_green": 50.33593358687364,
"WB_blue": 51.83252641949891,
"avg_brightness": 83.1379568446562,
"avg_perceived_brightness": 85.97517945924808,
"avg_hue": 6.733860813818914,
"avg_saturation": 11.420945158690257,
"avg_sharpness": 26.167174272129426,
"avg_highlights": 94.18936989524148,
"avg_shadow": 60.86544418334961,
"avg_temperature": 50.01037550206043,
"avg_noisy": 27.80209043716081,
"avg_exposure": 83.56836060635081
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_405.jpg",
"contrast": 40.250392191647165,
"WB_red": 48.1303710472222,
"WB_green": 50.52159860131352,
"WB_blue": 52.48379378916028,
"avg_brightness": 74.13857565202578,
"avg_perceived_brightness": 79.14378118359825,
"avg_hue": 28.333157914147336,
"avg_saturation": 13.746253345302456,
"avg_sharpness": 41.256679482885595,
"avg_highlights": 52.15839732776988,
"avg_shadow": 63.26673126220703,
"avg_temperature": 50.01445226480612,
"avg_noisy": 20.204118433454262,
"avg_exposure": 74.6062108313833
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_457.jpg",
"contrast": 50.118629081160215,
"WB_red": 48.59653016399227,
"WB_green": 50.154565418912696,
"WB_blue": 53.17862784572248,
"avg_brightness": 45.11712712703588,
"avg_perceived_brightness": 49.89648023060663,
"avg_hue": 25.375147066844317,
"avg_saturation": 23.12239987683694,
"avg_sharpness": 57.9748659930197,
"avg_highlights": 25.001609108664773,
"avg_shadow": 58.68082046508789,
"avg_temperature": 50.01494469539881,
"avg_noisy": 25.15758636230787,
"avg_exposure": 45.63392210544014
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_472.jpg",
"contrast": 37.2433756644065,
"WB_red": 56.72581769880244,
"WB_green": 46.27228585692787,
"WB_blue": 55.78318321562122,
"avg_brightness": 17.128579009705703,
"avg_perceived_brightness": 25.33156634096554,
"avg_hue": 38.82791521475317,
"avg_saturation": 77.63395336912063,
"avg_sharpness": 18.86898592879606,
"avg_highlights": 28.46865567294034,
"avg_shadow": 56.69831085205078,
"avg_temperature": 49.997394516595314,
"avg_noisy": 18.694714058996205,
"avg_exposure": 17.97759646977387
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_486.jpg",
"contrast": 53.733759767356325,
"WB_red": 44.10767460546046,
"WB_green": 52.090758016192964,
"WB_blue": 58.33425965926219,
"avg_brightness": 45.32772320003958,
"avg_perceived_brightness": 53.68828772280673,
"avg_hue": 16.45729986230206,
"avg_saturation": 40.52376807786208,
"avg_sharpness": 36.691715126649115,
"avg_highlights": 35.14423717151988,
"avg_shadow": 46.660186767578125,
"avg_temperature": 50.0468484400274,
"avg_noisy": 26.97224019694749,
"avg_exposure": 46.086897511036184
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_57.jpg",
"contrast": 56.9853220854583,
"WB_red": 58.8159180716154,
"WB_green": 46.67058087480473,
"WB_blue": 48.67295490245864,
"avg_brightness": 36.35092047183147,
"avg_perceived_brightness": 41.29948985538431,
"avg_hue": 39.25816627200343,
"avg_saturation": 42.936078162403504,
"avg_sharpness": 21.978832384315563,
"avg_highlights": 49.4837812943892,
"avg_shadow": 34.542518615722656,
"avg_temperature": 49.969960705781396,
"avg_noisy": 28.60439696838691,
"avg_exposure": 36.98316948635878
}
{
"File": ".\\dataset\\animal_customize_cat_filter\\animals_76.jpg",
"contrast": 35.59637450704945,
"WB_red": 45.67628053796047,
"WB_green": 51.19210783079706,
"WB_blue": 57.38785140417896,
"avg_brightness": 47.177013905699454,
"avg_perceived_brightness": 53.02168540484323,
"avg_hue": 18.52858530473366,
"avg_saturation": 27.550239158306212,
"avg_sharpness": 64.15141294937406,
"avg_highlights": 12.703718705610795,
"avg_shadow": 75.76203155517578,
"avg_temperature": 50.037958380459116,
"avg_noisy": 17.867984066283643,
"avg_exposure": 48.096659628643415
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5
dataset/changelog.txt Normal file
View File

@ -0,0 +1,5 @@
animals,54,511
people,51,500
food-drink,52,510
nature,51,500
architecture-interior,52,510

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

74
dataset_analysis.py Normal file
View File

@ -0,0 +1,74 @@
import pandas as pd
import json
def dataset_desc(file_path):
# Stores a list of JSON objects
json_objects = []
# A string that builds a single JSON object
current_json_str = ""
# Split different image data in json file
with open(file_path, 'r') as file:
for line in file:
# Removes whitespace at the beginning and end of a line
line = line.strip()
# If the line begins with '{' , it means that a new JSON object starts
if line.startswith("{"):
current_json_str = line
# If this line ends with '}' , it means that a JSON object ends
elif line.endswith("}"):
current_json_str += line
try:
# Parsing JSON strings
json_obj = json.loads(current_json_str)
json_objects.append(json_obj)
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}")
current_json_str = ""
# If it is part of a JSON object, it is added to the current JSON string
else:
current_json_str += line
# Convert a list of JSON objects to pandas DataFrame
df = pd.DataFrame(json_objects)
# Filter out numeric columns
numeric_df = df.select_dtypes(include='number')
# Create a list to collect data for all numeric columns
boxplot_data = []
# Create an empty DataFrame to store statistics
stats_df = pd.DataFrame(columns=[
'Parameter', 'Min', 'Max', 'Mean', 'Median', 'Mode', 'Range',
'Q1', 'Q3', 'IQR', 'Variance', 'Standard Deviation', 'Skewness'
])
# Generate statistics and draw graphs
for column in numeric_df.columns:
data = numeric_df[column].dropna()
boxplot_data.append(numeric_df[column].dropna())
# Computational statistics
stats = {
'Parameter': column,
'Min': data.min(),
'Max': data.max(),
'Mean': data.mean(),
'Median': data.median(),
'Mode': data.mode().values[0] if not data.mode().empty else float('nan'),
'Range': data.max() - data.min(),
'Q1': data.quantile(0.25),
'Q3': data.quantile(0.75),
'IQR': data.quantile(0.75) - data.quantile(0.25),
'Variance': data.var(),
'Standard Deviation': data.std(),
'Skewness': data.skew()
}
# Append the statistics to the stats DataFrame using concat
stats_df = pd.concat([stats_df, pd.DataFrame([stats])], ignore_index=True)
return stats_df, df

102
dataset_predict.py Normal file
View File

@ -0,0 +1,102 @@
import json
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPRegressor
def remove_outliers(df, threshold=3):
z_scores = np.abs((df - df.mean()) / df.std())
df_cleaned = df[(z_scores < threshold).all(axis=1)]
return df_cleaned
def optimal_val_predict(file_path, contrast, WB_red, WB_green, WB_blue, avg_brightness, avg_perceived_brightness,
avg_hue, avg_saturation, avg_sharpness, avg_highlights, avg_shadow, avg_temperature,
avg_noisy, avg_exposure):
# Stores a list of JSON objects
json_objects = []
# A string that builds a single JSON object
current_json_str = ""
# Split different image data in json file
with open(file_path, 'r') as file:
for line in file:
# Removes whitespace at the beginning and end of a line
line = line.strip()
# If the line begins with { , it means that a new JSON object starts
if line.startswith("{"):
current_json_str = line
# If this line ends with } , it means that a JSON object ends
elif line.endswith("}"):
current_json_str += line
try:
# Parsing JSON strings
json_obj = json.loads(current_json_str)
json_objects.append(json_obj)
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}")
current_json_str = ""
# If it is part of a JSON object, it is added to the current JSON string
else:
current_json_str += line
# Convert good image feature data to DataFrame
excellent_df = pd.DataFrame(json_objects).select_dtypes(include='number')
# Remove outliers from your DataFrame
cleaned_df = remove_outliers(excellent_df)
# Use excellent image feature data as training data
df = cleaned_df.copy()
target_df = cleaned_df
# Standardized feature
scaler_X = StandardScaler()
scaler_y = StandardScaler()
X_scaled = scaler_X.fit_transform(df)
y_scaled = scaler_y.fit_transform(target_df)
# Initialize and train the neural network model
model = MLPRegressor(hidden_layer_sizes=(100, 100), max_iter=1000, random_state=42)
model.fit(X_scaled, y_scaled)
# Feature data for new images
new_data = {
"contrast": [contrast],
"WB_red": [WB_red],
"WB_green": [WB_green],
"WB_blue": [WB_blue],
"avg_brightness": [avg_brightness],
"avg_perceived_brightness": [avg_perceived_brightness],
"avg_hue": [avg_hue],
"avg_saturation": [avg_saturation],
"avg_sharpness": [avg_sharpness],
"avg_highlights": [avg_highlights],
"avg_shadow": [avg_shadow],
"avg_temperature": [avg_temperature],
"avg_noisy": [avg_noisy],
"avg_exposure": [avg_exposure]
}
# convert to DataFrame
new_df = pd.DataFrame(new_data)
# Standardize feature data for new images
new_X_scaled = scaler_X.transform(new_df)
# Use the model to predict optimized features
optimized_features_scaled = model.predict(new_X_scaled)
# The features after anti-standardization optimization
optimized_features = scaler_y.inverse_transform(optimized_features_scaled)
# # Calculate the mean square error between the predicted value and the true value (Mean Squared Error, MSE)
# import numpy as np
# mse = np.mean((y_scaled - model.predict(X_scaled)) ** 2)
# print("Mean Squared Error (MSE):", mse)
# Displays optimized features
optimized_df = pd.DataFrame(optimized_features, columns=target_df.columns)
return optimized_df

199
export_func.py Normal file
View File

@ -0,0 +1,199 @@
import os
import sys
import tkinter as tk
from PIL import Image, ImageEnhance, ImageTk
from tkinter import filedialog, messagebox
from io import BytesIO
def save_image(image, output_image_path):
if image.mode == 'RGBA':
image = image.convert('RGB')
ext = os.path.splitext(output_image_path)[1].lower()
if ext in ['.jpg', '.jpeg']:
image.save(output_image_path, format='JPEG')
elif ext == '.png':
image.save(output_image_path, format='PNG')
else:
image.save(output_image_path)
root.quit()
root.destroy()
def compress_image(image, quality=85):
buffer = BytesIO()
if image.mode == 'RGBA':
image = image.convert('RGB')
image.save(buffer, format="JPEG", quality=quality)
buffer.seek(0)
return Image.open(buffer)
def adjust_sharpness(image, sharpness_factor=2.0):
enhancer = ImageEnhance.Sharpness(image)
return enhancer.enhance(sharpness_factor)
def resize_image(image, max_width, max_height):
ratio = min(max_width / image.width, max_height / image.height)
new_width = int(image.width * ratio)
new_height = int(image.height * ratio)
return image.resize((new_width, new_height), Image.LANCZOS), new_width, new_height
def update_image():
global img_tk
img = processed_image.copy()
# Apply compression
quality = compress_scale.get()
img = compress_image(img, int(quality))
# Apply sharpness
sharpness = clarity_scale.get() / 50.0 # scale from 0-100 to 0-2
img = adjust_sharpness(img, sharpness)
# Update the displayed image
img_tk = ImageTk.PhotoImage(img)
canvas_export.delete("current_image") # Delete the previous image
canvas_export.create_image(0, 0, anchor=tk.NW, image=img_tk, tags="current_image") # Add the new image with a tag
# Draw crop rectangle
canvas_export.delete("rect")
canvas_export.create_rectangle(start_x, start_y, end_x, end_y, outline='red', tag="rect")
def on_canvas_click(event):
global start_x, start_y
start_x = event.x
start_y = event.y
canvas_export.delete("rect")
def on_canvas_drag(event):
global start_x, start_y, end_x, end_y
end_x = event.x
end_y = event.y
canvas_export.delete("rect")
canvas_export.create_rectangle(start_x, start_y, end_x, end_y, outline='red', tag="rect")
def on_canvas_release(event):
global end_x, end_y
end_x = event.x
end_y = event.y
def crop_and_process_image(filename):
if processed_image is None:
messagebox.showwarning("Warning", "Please select an image first!")
return
# Get the coordinates of the selected region
x1 = min(start_x, end_x)
y1 = min(start_y, end_y)
x2 = max(start_x, end_x)
y2 = max(start_y, end_y)
cropped_img = processed_image.crop((x1, y1, x2, y2))
compress_quality = compress_scale.get()
sharpness_factor = clarity_scale.get() / 50.0 # scale from 0-100 to 0-2
cropped_img = compress_image(cropped_img, int(compress_quality))
cropped_img = adjust_sharpness(cropped_img, sharpness_factor)
base_name, extension = os.path.splitext(filename)
suggested_filename = base_name + "_processed" + extension
output_path = filedialog.asksaveasfilename(
title="Choose a file",
initialfile=suggested_filename,
filetypes=(("JPEG files", "*.jpg;*.jpeg"), ("PNG files", "*.png"), ("All files", "*.*"))
)
if output_path:
save_image(cropped_img, output_path)
messagebox.showinfo("Info", f"Image processed and saved to {output_path}")
def create_export_window(filename):
global original_image, processed_image, compress_scale, clarity_scale, canvas_export, start_x, start_y, end_x, end_y, canvas_frame, root, img_tk
root = tk.Tk()
root.title("Image Export Window")
root.configure(bg="#004c66")
root.iconbitmap('./icon/icon.ico')
# Convert the image buffer to a PIL Image
original_image = Image.open(filename)
# Resize the image to fit the canvas
processed_image, new_width, new_height = resize_image(original_image, 800, 600)
# Initialize cropping variables
start_x = 0
start_y = 0
end_x = new_width
end_y = new_height
# Set the root window geometry based on the image size
root.geometry(f"{new_width + 20}x{new_height + 200}")
# Set up the canvas frame
canvas_frame = tk.Frame(root, bg='white')
canvas_frame.pack(pady=10, fill=tk.BOTH, expand=True)
# Set up the canvas
canvas_export = tk.Canvas(canvas_frame, width=new_width, height=new_height, bg='white')
canvas_export.pack(fill=tk.BOTH, expand=True)
# Bind canvas events
canvas_export.bind("<Button-1>", on_canvas_click)
canvas_export.bind("<B1-Motion>", on_canvas_drag)
canvas_export.bind("<ButtonRelease-1>", on_canvas_release)
# Display the initial image
img_tk = ImageTk.PhotoImage(processed_image)
canvas_export.image = img_tk
canvas_export.create_image(0, 0, anchor=tk.NW, image=img_tk)
# Set up compression scale
compress_label = tk.Label(root, text="Compression", bg="yellow")
compress_label.pack(anchor='w', padx=10)
compress_scale = tk.Scale(root, from_=0, to=100, orient=tk.HORIZONTAL, bg="yellow",
command=lambda x: update_image())
compress_scale.set(85) # Set default value
compress_scale.pack(fill='x', padx=10)
# Set up clarity scale
clarity_label = tk.Label(root, text="Clarity", bg="yellow")
clarity_label.pack(anchor='w', padx=10)
clarity_scale = tk.Scale(root, from_=0, to=100, orient=tk.HORIZONTAL, bg="yellow", command=lambda x: update_image())
clarity_scale.set(50) # Set default value
clarity_scale.pack(fill='x', padx=10)
# Set up button frame
button_frame = tk.Frame(root, bg="#004c66")
button_frame.pack(pady=10)
# Set up confirm button
confirm_button = tk.Button(button_frame, text="Confirm", command=lambda: crop_and_process_image(filename),
bg="yellow")
confirm_button.pack(side='left', padx=10)
# Set up cancel button
cancel_button = tk.Button(button_frame, text="Cancel", command=root.destroy, bg="yellow")
cancel_button.pack(side='right', padx=10)
root.mainloop()
if __name__ == "__main__":
if len(sys.argv) < 2:
# Assuming the image file path is passed directly for testing
img_path = filedialog.askopenfilename(title="Select an Image", filetypes=(("Image files", "*.jpg;*.jpeg;*.png"), ("All files", "*.*")))
if img_path:
create_export_window(img_path)
else:
tmp_file_path = sys.argv[1]
create_export_window(tmp_file_path)

BIN
icon/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
icon/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
icon/select.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

633
main.py Normal file
View File

@ -0,0 +1,633 @@
import os
import uuid
import tempfile
import subprocess
import seaborn as sns
import cv2 as cv
import tkinter as tk
from tkinter import filedialog, messagebox
import customtkinter as ctk
import numpy as np
import pandas as pd
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import analyze_func as img_func
import dataset_predict as predict
import dataset_analysis
def raise_frame(next_frame):
next_frame.tkraise()
def on_close():
root.quit()
root.destroy()
def select_img():
# Select image file
filename = filedialog.askopenfilename(
title="Select An Image",
filetypes=(("Image", "*.png"), ("Image", "*.jpg"), ("Image", "*.jpeg"))
)
if filename:
# Read image file
global img_name
global original_img
img_name = os.path.basename(filename)
img_pil = Image.open(filename)
img = cv.cvtColor(np.array(img_pil), cv.COLOR_RGB2BGR)
original_img = img
if img is not None:
raise_frame(f_main)
plot_img_hist(img, f_main_left_top, 7, 3)
display_image(img, f_main_right_top)
# Get image parameter values
WB_red, WB_green, WB_blue = img_func.get_white_balance(img)
average_brightness = img_func.get_brightness(img)
contrast = img_func.get_contrast(img)
average_hue = img_func.get_hue(img)
average_saturation = img_func.get_saturation(img)
average_perceived_brightness = img_func.get_perceived_avg_brightness(img)
average_sharpen = img_func.get_sharpness(img)
average_highlights = img_func.get_highlights(img)
average_shadow = img_func.get_shadows(img)
average_temperature = img_func.get_color_temperature(img)
average_noisy = img_func.get_noise(img)
average_exposure = img_func.get_exposure(img)
# Display image parameter values
parameters = {
"Red": WB_red,
"Green": WB_green,
"Blue": WB_blue,
"Contrast": contrast,
"Brightness": average_brightness,
"Perceived Brightness": average_perceived_brightness,
"Hue": average_hue,
"Saturation": average_saturation,
"Sharpness": average_sharpen,
"Highlight": average_highlights,
"Shadow": average_shadow,
"Temperature": average_temperature,
"Noise": average_noisy,
"Exposure": average_exposure
}
display_parameters(f_main_left_bottom, parameters, img)
def display_parameters(frame, parameters, img):
# Clear previous content
for widget in frame.winfo_children():
widget.destroy()
global img_buffer
img_buffer = img
# Create a scrollable canvas
canvas = ctk.CTkCanvas(frame, highlightthickness=0, bg="white")
scrollbar = ctk.CTkScrollbar(frame, fg_color="white", command=canvas.yview)
scrollable_frame = ctk.CTkFrame(canvas, fg_color="white")
scrollable_frame.bind("<Configure>", lambda e=None: canvas.configure(scrollregion=canvas.bbox("all")))
# Function to change background color on focus
def on_focus_in(event):
event.widget.configure(foreground="black")
def on_focus_out(event):
event.widget.configure(foreground="gray")
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Pack the canvas and scrollbar
canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
scrollbar.pack(side=ctk.LEFT, fill=ctk.Y)
# Adding parameters to the scrollable frame
for i, (param, value) in enumerate(parameters.items()):
# Parameter's name label
label = ctk.CTkLabel(scrollable_frame, text=f"{param}", anchor="w", fg_color="white")
label.grid(row=i, column=0, padx=10, pady=5, sticky="w")
# Parameter's value textbox
value_str = f"{value}"
textbox = ctk.CTkTextbox(scrollable_frame, height=1, width=160, wrap="none", fg_color="#f0f0f0",
text_color="gray")
textbox.insert("1.0", value_str)
textbox.grid(row=i, column=1, padx=10, pady=5, sticky="w")
# Bind focus in and focus out events
textbox.bind("<FocusIn>", on_focus_in)
textbox.bind("<FocusOut>", on_focus_out)
textbox.bind("<FocusOut>",
lambda event=None, param1=param, textbox1=textbox: param_updator(param1, textbox1))
scrollable_frame.update_idletasks()
def param_updator(param, textbox):
# Get new parameter value
try:
new_val = float(textbox.get("1.0", "end-1c"))
except ValueError:
messagebox.showerror("Invalid input", f"Invalid value for {param}: {textbox.get('1.0', 'end-1c')}")
return
# Pass it to its corresponding 'modify' function
global img_buffer
modified_img = None
if param == "Red":
modified_img = img_func.modify_white_balance(img_buffer, new_val, -1, -1)
elif param == "Green":
modified_img = img_func.modify_white_balance(img_buffer, -1, new_val, -1)
elif param == "Blue":
modified_img = img_func.modify_white_balance(img_buffer, -1, -1, new_val)
elif param == "Contrast":
modified_img = img_func.modify_contrast(img_buffer, new_val)
elif param == "Brightness":
modified_img = img_func.modify_brightness(img_buffer, new_val)
elif param == "Perceived Brightness":
modified_img = img_func.modify_perceived_avg_brightness(img_buffer, new_val)
elif param == "Hue":
modified_img = img_func.modify_hue(img_buffer, new_val)
elif param == "Saturation":
modified_img = img_func.modify_saturation(img_buffer, new_val)
elif param == "Sharpness":
modified_img = img_func.modify_sharpness(img_buffer, new_val)
elif param == "Highlight":
modified_img = img_func.modify_highlights(img_buffer, new_val)
elif param == "Shadow":
modified_img = img_func.modify_shadows(img_buffer, new_val)
elif param == "Temperature":
modified_img = img_func.modify_color_temperature(img_buffer, new_val)
if param == "Noise":
modified_img = img_func.modify_noise(img_buffer, new_val)
elif param == "Exposure":
modified_img = img_func.modify_exposure(img_buffer, new_val)
else:
pass
# Update img_buffer & Refresh the window
if modified_img is not None:
img_buffer = modified_img
plot_img_hist(img_buffer, f_main_left_top, 7, 3)
display_image(img_buffer, f_main_right_top)
def display_image(img, frame):
# Clear previous image
for widget in frame.winfo_children():
widget.destroy()
# Convert the image to RGB format
img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
img_pil = Image.fromarray(img_rgb)
# Get the height and width of the image
height, width = img.shape[:2]
# Calculate the maximum width and height
if height > width:
max_width = 380
else:
max_width = 500
img_ratio = width / height
max_height = int(max_width / img_ratio)
# Display image in the frame
img_tk = ctk.CTkImage(img_pil, size=(max_width, max_height))
label = ctk.CTkLabel(frame, image=img_tk, text="")
label.image = img_tk
label.pack(padx=10, pady=10, fill='x')
def plot_img_hist(img, frame, width, height):
# Clear previous plots
for widget in frame.winfo_children():
widget.destroy()
plt.figure(figsize=(width, height))
# Plot grayscale hist
plt.hist(img.ravel(), 256, [0, 256])
# Plot color hist
color = ('blue', 'green', 'red')
for i, color in enumerate(color):
hist = cv.calcHist([img], [i], None, [256], [0, 256])
plt.plot(hist, color=color)
plt.xlabel("Bins")
plt.ylabel("Pixel Number")
# Hide the top and right spines
ax = plt.gca()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Get the current figure & draw & close
figure = plt.gcf()
canvas = FigureCanvasTkAgg(figure, master=frame)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
plt.close()
def revert_to_original():
# Revert to original_img
global original_img
plot_img_hist(original_img, f_main_left_top, 7, 3)
display_image(original_img, f_main_right_top)
# Get image parameter values
WB_red, WB_green, WB_blue = img_func.get_white_balance(original_img)
average_brightness = img_func.get_brightness(original_img)
contrast = img_func.get_contrast(original_img)
average_hue = img_func.get_hue(original_img)
average_saturation = img_func.get_saturation(original_img)
average_perceived_brightness = img_func.get_perceived_avg_brightness(original_img)
average_sharpen = img_func.get_sharpness(original_img)
average_highlights = img_func.get_highlights(original_img)
average_shadow = img_func.get_shadows(original_img)
average_temperature = img_func.get_color_temperature(original_img)
average_noisy = img_func.get_noise(original_img)
average_exposure = img_func.get_exposure(original_img)
# Display image parameter values
parameters = {
"Red": WB_red,
"Green": WB_green,
"Blue": WB_blue,
"Contrast": contrast,
"Brightness": average_brightness,
"Perceived Brightness": average_perceived_brightness,
"Hue": average_hue,
"Saturation": average_saturation,
"Sharpness": average_sharpen,
"Highlight": average_highlights,
"Shadow": average_shadow,
"Temperature": average_temperature,
"Noise": average_noisy,
"Exposure": average_exposure
}
display_parameters(f_main_left_bottom, parameters, original_img)
def auto_optimize():
# Get current image
global img_buffer
# Check filter data availability
json_dataset_path = ".\\dataset\\" + combobox.get() + "\\" + combobox.get() + "_result.json"
if not os.path.exists(json_dataset_path):
tk.messagebox.showinfo("Cannot find dataset",
"Cannot find json data file for this dataset. Check & Update Dataset.")
return
# Get image parameter values
WB_red, WB_green, WB_blue = img_func.get_white_balance(img_buffer)
avg_brightness = img_func.get_brightness(img_buffer)
contrast = img_func.get_contrast(img_buffer)
avg_hue = img_func.get_hue(img_buffer)
avg_saturation = img_func.get_saturation(img_buffer)
avg_perceived_brightness = img_func.get_perceived_avg_brightness(img_buffer)
avg_sharpness = img_func.get_sharpness(img_buffer)
avg_highlights = img_func.get_highlights(img_buffer)
avg_shadow = img_func.get_shadows(img_buffer)
avg_temperature = img_func.get_color_temperature(img_buffer)
avg_noisy = img_func.get_noise(img_buffer)
avg_exposure = img_func.get_exposure(img_buffer)
# Pass the parameters & Predict optimal values
optimal_vals = predict.optimal_val_predict(json_dataset_path, contrast, WB_red, WB_green, WB_blue,
avg_brightness, avg_perceived_brightness, avg_hue,
avg_saturation, avg_sharpness, avg_highlights, avg_shadow,
avg_temperature, avg_noisy, avg_exposure)
# Ensure optimal_vals contains scalar values
optimal_vals = {key: val.item() if isinstance(val, pd.Series) else val for key, val in optimal_vals.items()}
# Modify image with optimal values
modified_img = img_func.modify_hue(img_buffer, optimal_vals["avg_hue"])
modified_img = img_func.modify_sharpness(modified_img, optimal_vals["avg_sharpness"])
modified_img = img_func.modify_color_temperature(modified_img, optimal_vals["avg_temperature"])
modified_img = img_func.modify_exposure(modified_img, optimal_vals["avg_exposure"])
modified_img = img_func.modify_white_balance(modified_img, optimal_vals["WB_red"],
optimal_vals["WB_green"], optimal_vals["WB_blue"])
modified_img = img_func.modify_contrast(modified_img, optimal_vals["contrast"])
modified_img = img_func.modify_saturation(modified_img, optimal_vals["avg_saturation"])
modified_img = img_func.modify_highlights(modified_img, optimal_vals["avg_highlights"])
modified_img = img_func.modify_noise(modified_img, optimal_vals["avg_noisy"])
modified_img = img_func.modify_brightness(modified_img, optimal_vals["avg_brightness"])
modified_img = img_func.modify_shadows(modified_img, optimal_vals["avg_shadow"])
modified_img = img_func.modify_perceived_avg_brightness(modified_img, optimal_vals["avg_perceived_brightness"])
# Update Window
if modified_img is not None:
img_buffer = modified_img
plot_img_hist(img_buffer, f_main_left_top, 7, 3)
display_image(img_buffer, f_main_right_top)
# Display image parameter values
parameters = {
"Red": WB_red,
"Green": WB_green,
"Blue": WB_blue,
"Contrast": contrast,
"Brightness": avg_brightness,
"Perceived Brightness": avg_perceived_brightness,
"Hue": avg_hue,
"Saturation": avg_saturation,
"Sharpness": avg_sharpness,
"Highlight": avg_highlights,
"Shadow": avg_shadow,
"Temperature": avg_temperature,
"Noise": avg_noisy,
"Exposure": avg_exposure
}
display_parameters(f_main_left_bottom, parameters, img_buffer)
def export_img():
if img_buffer is not None:
# Create a temporary address & Pass the temporarily stored image file
with tempfile.TemporaryDirectory() as temp_dir:
# Encode the original image name using UTF-8
if all(ord(char) < 128 for char in img_name):
# If all characters in img_name are ASCII characters
encoded_img_name = img_name.encode('utf-8')
else:
# If img_name contains non-ASCII characters
name, extension = os.path.splitext(img_name)
uuid_bytes = str(uuid.uuid4()).encode('utf-8')
encoded_img_name = uuid_bytes + extension.encode('utf-8')
encoded_img_name_str = encoded_img_name.decode('utf-8')
tmp_file_path = os.path.join(temp_dir, encoded_img_name_str)
cv.imwrite(tmp_file_path, img_buffer)
# Start Export Window
try:
subprocess.run(['python', 'export_func.py', tmp_file_path], capture_output=True, text=True)
except Exception as e:
messagebox.showerror("Exception", "Error occurred: " + str(e))
def dataset_select(value):
# Load the dataset
json_dataset_path = os.path.join(".\\dataset", value, f"{value}_result.json")
if not os.path.exists(json_dataset_path):
tk.messagebox.showinfo("Cannot find dataset",
"Cannot find json data file for this dataset. Check & Update Dataset.")
return
stats, original_stats = dataset_analysis.dataset_desc(json_dataset_path)
# Create a combobox to select the parameter
parameter_label = ctk.CTkLabel(f_dataset, text="Select Parameter:")
parameter_label.pack(padx=20, pady=10, anchor="w")
# Clear previous widgets
for widget in f_dataset.winfo_children():
if widget != home and widget != update and widget != crawler and widget != combobox_m:
widget.destroy()
# Display the box plot of all numeric columns
plt.figure(figsize=(18, 4))
plt.xticks(fontsize=8)
sns.boxplot(data=original_stats, color='blue')
plt.ylabel('Values')
plt.grid(True)
plt.tight_layout()
boxplot_canvas = FigureCanvasTkAgg(plt.gcf(), master=f_dataset)
boxplot_canvas.draw()
boxplot_canvas.get_tk_widget().pack()
plt.close()
# Update & Pack parameter_combobox
global parameter_combobox
parameter_combobox = ctk.CTkComboBox(f_dataset, values=stats['Parameter'].tolist(),
command=lambda param: display_parameter_stats(param, stats,
parameter_combobox))
parameter_combobox.pack(padx=20, pady=10, fill="x")
# Initial display of the first parameter's stats
if not stats.empty:
display_parameter_stats(stats['Parameter'].iloc[0], stats, parameter_combobox)
def display_parameter_stats(param, stats, parameter_combobox):
# Find the row corresponding to the selected parameter
param_stats = stats[stats['Parameter'] == param]
if param_stats.empty:
return
# Extract the parameter stats
param_values = param_stats.iloc[0].to_dict()
for widget in f_dataset.winfo_children():
if isinstance(widget, (ctk.CTkCanvas, ctk.CTkScrollbar)) and widget != parameter_combobox:
widget.destroy()
# Create a canvas and a scrollbar
canvas = ctk.CTkCanvas(f_dataset, bg="white", highlightthickness=0)
scrollbar = ctk.CTkScrollbar(f_dataset, fg_color="white", command=canvas.yview)
scrollable_frame = ctk.CTkFrame(canvas, fg_color="white")
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
# Place the scrollable frame in the canvas
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Pack the canvas and scrollbar
canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
scrollbar.pack(side=ctk.RIGHT, fill=ctk.Y)
# Display the stats for the selected parameter inside the scrollable frame
for key, value in param_values.items():
if key == 'Parameter':
continue
# Parameter name label
label = ctk.CTkLabel(scrollable_frame, text=f"{key}:", anchor="w", fg_color="white")
label.pack(padx=20, pady=5, anchor="w")
# Parameter value textbox
value_str = f"{value:.4f}" if isinstance(value, (int, float)) else f"{value}"
textbox = ctk.CTkTextbox(scrollable_frame, height=1, width=160, wrap="none", fg_color="#f0f0f0",
text_color="black")
textbox.insert("1.0", value_str)
textbox.pack(padx=20, pady=5, anchor="w")
textbox.configure(state="disabled")
# Display the histogram for the parameter
plt.figure(figsize=(6, 4))
sns.histplot(data=stats[key].dropna(), kde=True, bins=30, color='blue')
plt.ylabel('Frequency')
plt.grid(True)
plt.tight_layout()
hist_canvas = FigureCanvasTkAgg(plt.gcf(), master=scrollable_frame)
hist_canvas.draw()
hist_canvas.get_tk_widget().pack()
plt.close()
def dataset_update():
# Create waiting window
update.configure(text="Processing", fg_color="red")
update.update()
# Start Tasks
img_func.process_images_in_folders(root_dir)
global subfolders
subfolders = [sub_folder for sub_folder in os.listdir(root_dir) if
os.path.isdir(os.path.join(root_dir, sub_folder))]
combobox.configure(values=subfolders)
combobox_m.configure(values=subfolders)
combobox.update()
combobox_m.update()
dataset_select(combobox_m.get())
# End Waiting Window
update.configure(text="Update Dataset", fg_color="#3b8ed0")
update.update()
if __name__ == "__main__":
# Main window
img_buffer = None
bg_color = "white"
root = ctk.CTk(bg_color)
root.title("Image Optimizer")
root.minsize(800, 500)
root.iconbitmap('./icon/icon.ico')
root.protocol("WM_DELETE_WINDOW", on_close)
# Frames
f_wizard = ctk.CTkFrame(root, fg_color=bg_color)
f_main = ctk.CTkFrame(root, fg_color=bg_color)
f_dataset = ctk.CTkFrame(root, fg_color=bg_color)
for f in (f_wizard, f_main, f_dataset):
f.grid(row=0, column=0, sticky="nsew")
# Configure grid to expand
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
# Predefined root directory
root_dir = "./dataset"
subfolders = [sub_folder for sub_folder in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, sub_folder))]
# Wizard Interface
f_wizard_bg_color = "white"
# Wizard Interface Left
f_wizard_left = ctk.CTkFrame(f_wizard, fg_color=f_wizard_bg_color)
f_wizard_left.grid(row=0, column=0, sticky="nsew")
f_wizard_left.grid_rowconfigure(0, weight=1)
f_wizard_left.grid_columnconfigure(0, weight=1)
# Wizard Interface Right
f_wizard_right = ctk.CTkFrame(f_wizard, fg_color=f_wizard_bg_color)
f_wizard_right.grid(row=0, column=1, sticky="nsew")
f_wizard_right.grid_rowconfigure(0, weight=1)
f_wizard_right.grid_columnconfigure(0, weight=1)
# Configure grid in f_wizard
f_wizard.grid_rowconfigure(0, weight=1)
f_wizard.grid_columnconfigure(0, weight=1)
f_wizard.grid_columnconfigure(1, weight=1)
# Load Wizard Interface Icons
icon_select = ctk.CTkImage(Image.open("./icon/select.png"), size=(100, 100))
icon_db_man = ctk.CTkImage(Image.open("./icon/edit.png"), size=(100, 100))
# Buttons with icons and custom styles
select_img_button = ctk.CTkButton(
f_wizard_left, text="Select An Image", command=select_img,
image=icon_select, compound="top", fg_color="transparent", text_color="black", hover_color="lightblue"
)
select_img_button.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
db_manage_button = ctk.CTkButton(
f_wizard_right, text="Dataset Management", command=lambda: raise_frame(f_dataset),
image=icon_db_man, compound="top", fg_color="transparent", text_color="black", hover_color="lightblue"
)
db_manage_button.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
# Main Interface
f_main_bg_color = "white"
# Main Interface Left
f_main_left = ctk.CTkFrame(f_main, fg_color=f_main_bg_color)
f_main_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
f_main_left.grid_rowconfigure(0, weight=1)
f_main_left.grid_rowconfigure(1, weight=1) # Button
f_main_left.grid_columnconfigure(0, weight=1)
# Main Interface Left Top
f_main_left_top = ctk.CTkFrame(f_main_left, fg_color=f_main_bg_color)
f_main_left_top.grid(row=0, column=0, sticky="nsew")
f_main_left_top.grid_rowconfigure(0, weight=1)
f_main_left_top.grid_columnconfigure(0, weight=1)
# Main Interface Left Bottom
f_main_left_bottom = ctk.CTkFrame(f_main_left, fg_color=f_main_bg_color)
f_main_left_bottom.grid(row=1, column=0, sticky="nsew") # Button
f_main_left_bottom.grid_rowconfigure(0, weight=1)
f_main_left_bottom.grid_columnconfigure(0, weight=1)
# Main Interface Right
f_main_right = ctk.CTkFrame(f_main, fg_color=f_main_bg_color)
f_main_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
f_main_right.grid_rowconfigure(0, weight=1)
f_main_right.grid_rowconfigure(1, weight=1)
f_main_right.grid_columnconfigure(0, weight=1)
# Main Interface Right Top
f_main_right_top = ctk.CTkFrame(f_main_right, fg_color=f_main_bg_color)
f_main_right_top.grid(row=0, column=0, sticky="nsew")
f_main_right_top.grid_rowconfigure(0, weight=1)
f_main_right_top.grid_columnconfigure(0, weight=1)
# Main Interface Right Bottom
f_main_right_bottom = ctk.CTkFrame(f_main_right, fg_color=f_main_bg_color)
f_main_right_bottom.grid(row=1, column=0, sticky="nsew")
f_main_right_bottom.grid_rowconfigure(0, weight=1)
f_main_right_bottom.grid_columnconfigure(0, weight=1)
# Combobox to display sub-folder names
combobox = ctk.CTkComboBox(f_main_right_bottom, values=subfolders)
combobox.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
auto_button = ctk.CTkButton(f_main_right_bottom, text="Auto Optimization", command=auto_optimize)
auto_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
revert_button = ctk.CTkButton(f_main_right_bottom, text="Revert To Original", command=lambda: revert_to_original())
revert_button.grid(row=3, column=0, padx=20, pady=10, sticky="ew")
export_button = ctk.CTkButton(f_main_right_bottom, text="Export Image", command=export_img)
export_button.grid(row=4, column=0, padx=20, pady=10, sticky="ew")
export_button = ctk.CTkButton(f_main_right_bottom, text="Discard", command=lambda: raise_frame(f_wizard))
export_button.grid(row=5, column=0, padx=20, pady=10, sticky="ew")
# Configure grid in f_main
f_main.grid_rowconfigure(0, weight=1)
f_main.grid_columnconfigure(0, weight=1)
f_main.grid_columnconfigure(1, weight=1)
# Database Management Interface
# Back To Wizard Button
home = ctk.CTkButton(f_dataset, text="Back To Wizard", command=lambda: raise_frame(f_wizard))
home.pack(padx=20, pady=5, fill='x')
# Crawler Button
crawler = ctk.CTkButton(f_dataset, text="Get More Images from Unsplash",
command=lambda: subprocess.run(['python', 'crawler.py']))
crawler.pack(padx=20, pady=5, fill='x')
# Dataset Update Button
update = ctk.CTkButton(f_dataset, text="Update Dataset", command=dataset_update)
update.pack(padx=20, pady=5, fill='x')
# Combobox to display sub-folder names
combobox_m = ctk.CTkComboBox(f_dataset, values=subfolders, command=lambda value: dataset_select(value))
combobox_m.pack(padx=20, pady=5, fill='x')
dataset_select(combobox_m.get())
raise_frame(f_wizard)
root.mainloop()

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
customtkinter==5.2.2
matplotlib==3.8.2
numpy==1.24.3
opencv_python==4.9.0.80
pandas==1.5.3
Pillow==10.3.0
Requests==2.32.2
scikit_learn==1.4.0
seaborn==0.13.2