"""
.. module:: Katna.image
:platform: OS X
:synopsis: This module has functions related to smart cropping
"""
import os
import cv2
import numpy as np
from Katna.decorators import FileDecorators
from Katna.feature_list import FeatureList
from Katna.filter_list import FilterList
from Katna.crop_extractor import CropExtractor
from Katna.crop_selector import CropSelector
import Katna.config as config
from Katna.decorators import DebugDecorators
[docs]class UserFiltersEnum:
"""Enum class for filters"""
text = "TextDetector"
[docs]class Image(object):
"""Class for all image cropping operations
:param object: base class inheritance
:type object: class:`Object`
"""
def __init__(self, disable_text=True):
"""Constructor for image files"""
featureList = FeatureList()
filterList = FilterList()
self.user_filters_enum = UserFiltersEnum()
self.crop_extractor = CropExtractor()
self.crop_selector = CropSelector()
self.features = featureList.get_features()
self.definedFilters = filterList.get_filters()
def _get_crop_specs(
self, image_height, image_width, ratio_height, ratio_width, is_height_small=True
):
"""Internal function to create the crop specs for a given aspect ratio
:param image_height: height of image
:type image_height: int, required
:param image_width: width of image
:type image_width: int, required
:param ratio_height: aspect ratio height (eg. 3 from 4:3)
:type ratio_height: int, required
:param ratio_width: aspect ratio width (eg. 4 from 4:3)
:type ratio_width: int, required
:param is_height_small: parameter to check if crop dimension should be reduced wrt height[default=True]
:type is_height_small: boolean, required
:return: list of crop height and crop width
:rtype:list of tuples
"""
# multiplication factor by which height/width of crop should be decreased to get crop specs
multiply_by = 1
crop_list_tuple = []
# Calculating the height and width ratio wrt aspect ratio
hr, wr = image_height / ratio_height, image_width / ratio_width
# print("hr, wr",hr, wr)
# Check if height is smaller than the width.If yes, interchange height and width.
if not is_height_small:
image_height, image_width = image_width, image_height
hr, wr = wr, hr
crop_height, crop_width = image_height, hr * ratio_width
# Decreasing the height and width for crops while checking it don't get small by 1/(min) of image height/width
while True:
if not (
(crop_height >= (image_height // config.Image.min_image_to_crop_factor))
and (
crop_width >= (image_width // config.Image.min_image_to_crop_factor)
)
):
break
crop_height, crop_width = (
int(crop_height),
int((ratio_width / ratio_height) * crop_height),
)
crop_list_tuple.append((crop_height, crop_width))
crop_height /= multiply_by
crop_height, crop_width = (
int(crop_height),
int((ratio_width / ratio_height) * crop_height),
)
multiply_by += config.Image.crop_height_reduction_factor_in_each_iteration
return crop_list_tuple
# Apply optional Debug mode decorator , If config=DEBUG is true this decorator
# will populate internal variables of Image module.debug_images with debug images
# Which you can see by opencv Imshow to check if every feature is working as expected
[docs] @DebugDecorators.add_optional_debug_images_for_image_module
def crop_image_from_cvimage(
self,
input_image,
crop_width,
crop_height,
num_of_crops,
filters=[],
down_sample_factor=config.Image.down_sample_factor,
):
"""smartly crops the imaged based on the specification - width and height
:param input_image: Input image
:type input_image: numpy array, required
:param crop_width: output crop width
:type crop_width: int
:param crop_height: output crop heigh
:type crop_height: int
:param num_of_crops: number of crops required
:type num_of_crops: int
:param filters: filters to be applied for cropping(only returns crops containing english text where the crop rectangle doesn't cut the text)
:type filters: list (eg. ['text'])
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:return: crop list
:rtype: list of structure crop_rect
"""
self.crop_extractor.down_sample_factor = down_sample_factor
if (
input_image.shape[0] + 5 <= crop_height
or input_image.shape[1] + 5 <= crop_width
):
# print(
# "Error: crop width or crop height larger than Image",
# "input_image.shape",
# input_image.shape,
# "crop_width",
# crop_width,
# "crop_height",
# crop_height,
# )
return []
extracted_candidate_crops = self.crop_extractor.extract_candidate_crops(
input_image, crop_width, crop_height, self.features
)
# print(extracted_candidate_crops)
# text: TextDetector
# dummy: DummyDetector
self.filters = []
for x in filters:
try:
self.filters.append(eval("self.user_filters_enum." + x))
except AttributeError as e:
print(str(e))
# self.filters = [eval("user_filters_enum."+x) for x in filters]
crops_list = self.crop_selector.select_candidate_crops(
input_image,
num_of_crops,
extracted_candidate_crops,
self.definedFilters,
self.filters,
)
return crops_list
def _extract_crop_for_files_iterator(
self,
list_of_files,
crop_width,
crop_height,
num_of_crops,
filters,
down_sample_factor,
):
"""Generator which yields crop data / error for filepaths in a list
:param list_of_files: list of files to process for crop
:type list_of_files: list, required
:param crop_width: output crop width
:type crop_width: int
:param crop_height: output crop height
:type crop_height: int
:param num_of_crops: number of crops required
:type num_of_crops: int
:param filters: filters to be applied for cropping(checks if image contains english text and the crop rectangle doesn't cut the text)
:type filters: list (eg. ['text'])
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:yield: dict containing error (if any), data ,and filepath of image processed
:rtype: dict
"""
for filepath in list_of_files:
print("Running for : ", filepath)
try:
crop_list = self._crop_image(
filepath,
crop_width,
crop_height,
num_of_crops,
filters,
down_sample_factor,
)
yield {"crops": crop_list, "error": None,"filepath": filepath}
except Exception as e:
yield {"crops": crop_list, "error": e,"filepath": filepath}
[docs] @FileDecorators.validate_dir_path
def crop_image_from_dir(
self,
dir_path,
crop_width,
crop_height,
num_of_crops,
writer,
filters=[],
down_sample_factor=config.Image.down_sample_factor,
):
"""smartly crops all the images (inside a directory) based on the specification - width and height
:param dir_path: Input Directory path
:type dir_path: str, required
:param crop_width: output crop width
:type crop_width: int
:param crop_height: output crop height
:type crop_height: int
:param num_of_crops: number of crops required
:type num_of_crops: int
:param writer: number of crops required
:type writer: int
:param filters: filters to be applied for cropping(checks if image contains english text and the crop rectangle doesn't cut the text)
:type filters: list (eg. ['text'])
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:return: crop dict with key as filepath and crop list for the file
:rtype: dict
"""
valid_files = []
all_crops = {}
for path, subdirs, files in os.walk(dir_path):
for filename in files:
filepath = os.path.join(path, filename)
if self._check_if_valid_image(filepath):
valid_files.append(filepath)
if len(valid_files) > 0:
generator = self._extract_crop_for_files_iterator(
valid_files,
crop_width,
crop_height,
num_of_crops,
filters,
down_sample_factor
)
for data in generator:
file_path = data["filepath"]
file_crops = data["crops"]
error = data["error"]
if error is None:
writer.write(file_path, file_crops)
print("Completed processing for : ", file_path)
else:
print("Error processing file : ", file_path)
print(error)
else:
print("All the files in directory %s are invalid video files" % dir_path)
def _crop_image(
self,
file_path,
crop_width,
crop_height,
num_of_crops,
filters=[],
down_sample_factor=config.Image.down_sample_factor,
):
"""smartly crops the imaged based on the specification - width and height
:param file_path: Input file path
:type file_path: str, required
:param crop_width: output crop width
:type crop_width: int
:param crop_height: output crop heigh
:type crop_height: int
:param num_of_crops: number of crops required
:type num_of_crops: int
:param filters: filters to be applied for cropping(checks if image contains english text and the crop rectangle doesn't cut the text)
:type filters: list (eg. ['text'])
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:return: crop list
:rtype: list of structure crop_rect
"""
imgFile = cv2.imread(file_path)
crop_list = self.crop_image_from_cvimage(
input_image=imgFile,
crop_width=crop_width,
crop_height=crop_height,
num_of_crops=num_of_crops,
filters=filters,
down_sample_factor=down_sample_factor,
)
return crop_list
[docs] @FileDecorators.validate_file_path
def crop_image(
self,
file_path,
crop_width,
crop_height,
num_of_crops,
writer,
filters=[],
down_sample_factor=config.Image.down_sample_factor,
):
"""smartly crops the imaged based on the specification - width and height
:param file_path: Input file path
:type file_path: str, required
:param crop_width: output crop width
:type crop_width: int
:param crop_height: output crop heigh
:type crop_height: int
:param num_of_crops: number of crops required
:type num_of_crops: int
:param writer: writer object to process data
:type writer: Writer, required
:param filters: filters to be applied for cropping(checks if image contains english text and the crop rectangle doesn't cut the text)
:type filters: list (eg. ['text'])
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:return: crop list
:rtype: list of structure crop_rect
"""
crop_list = self._crop_image(
file_path,
crop_width,
crop_height,
num_of_crops,
filters=[],
down_sample_factor=config.Image.down_sample_factor
)
writer.write(file_path, crop_list)
[docs] @FileDecorators.validate_file_path
def crop_image_with_aspect(
self,
file_path,
crop_aspect_ratio,
num_of_crops,
writer,
filters=[],
down_sample_factor=8
):
"""smartly crops the imaged based on the aspect ratio and returns number of specified crops for each crop spec found in the image with
the specified aspect ratio
:param file_path: Input file path
:type file_path: str, required
:param crop_aspect_ratio: output crop ratio
:type crop_aspect_ratio: str (eg. '4:3')
:param num_of_crops: number of crops required
:type num_of_crops: int
:param filters: filters to be applied for cropping(checks if image contains english text and the crop rectangle doesn't cut the text)
:type filters: list (eg. ['text'])
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:param writer: writer to process the image
:type num_of_crops: Writer, required
:return: crop list
:rtype: list of structure crop_rect
"""
imgFile = cv2.imread(file_path)
image_height, image_width, _ = imgFile.shape
ratio_width, ratio_height = map(int, crop_aspect_ratio.split(":"))
crop_list = self._generate_crop_options_given_for_given_aspect_ratio(
imgFile,
image_width,
image_height,
ratio_width,
ratio_height,
num_of_crops=num_of_crops,
filters=filters,
down_sample_factor=down_sample_factor,
)
sorted_list = sorted(crop_list, key=lambda x: float(x.score), reverse=True)
crop_list = sorted_list[:num_of_crops]
writer.write(file_path, crop_list)
#
[docs] @FileDecorators.validate_file_path
def save_crop_to_disk(self, crop_rect, frame, file_path, file_name, file_ext, rescale=False):
"""saves an in-memory crop on drive.
:param crop_rect: In-memory crop_rect.
:type crop_rect: crop_rect, required
:param frame: In-memory input image.
:type frame: numpy.ndarray, required
:param file_name: name of the image.
:type file_name: str, required
:param file_path: Folder location where files needs to be saved
:type file_path: str, required
:param file_ext: File extension indicating the file type for example - '.jpg'
:type file_ext: str, required
:return: None
"""
cropped_img = crop_rect.get_image_crop(frame)
file_full_path = os.path.join(file_path, file_name + file_ext)
cv2.imwrite(file_full_path, cropped_img)
[docs] @FileDecorators.validate_file_path
def resize_image(
self,
file_path,
target_width,
target_height,
down_sample_factor=config.Image.down_sample_factor,
):
"""smartly resizes the image based on the specification - width and height
:param file_path: Input file path
:type file_path: str, required
:param target_width: output image width
:type target_width: int
:param target_height: output image height
:type target_height: int
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:return: resized image
:rtype: cv_image
"""
if not self._check_if_valid_image(file_path):
print("Error: Invalid Image, check image path: ", file_path)
return
imgFile = cv2.imread(file_path)
input_image_height, input_image_width, _ = imgFile.shape
target_image_aspect_ratio = target_width / target_height
input_image_aspect_ratio = input_image_width / input_image_height
if input_image_aspect_ratio == target_image_aspect_ratio:
target_image = cv2.resize(imgFile, (target_width, target_height))
return target_image
else:
crop_list = self._generate_crop_options_given_for_given_aspect_ratio(
imgFile,
input_image_width,
input_image_height,
target_width,
target_height,
num_of_crops=1,
filters=[],
down_sample_factor=down_sample_factor,
)
# From list of crop options sort and get best crop using crop score variables in each
# crop option
sorted_list = sorted(crop_list, key=lambda x: float(x.score), reverse=True)
# Get top crop image
resized_image = sorted_list[0].get_image_crop(imgFile)
target_image = cv2.resize(resized_image, (target_width, target_height))
return target_image
def _generate_crop_options_given_for_given_aspect_ratio(
self,
imgFile,
input_image_width,
input_image_height,
target_width,
target_height,
num_of_crops,
filters,
down_sample_factor,
):
""" Internal function to which for given aspect ratio (target_width/target_height)
Generates ,scores and returns list of image crops
:param imgFile: Input image
:type imgFile: opencv image
:param input_image_width: input image width
:type input_image_width: int
:param input_image_height: input image height
:type input_image_height: int
:param target_width: target aspect ratio width
:type target_width: int
:param target_height: target aspect ratio height
:type target_height: int
:param num_of_crops: number of crop needed in the end
:type num_of_crops: int
:param filters: filters
:type filters: list of filters
:param down_sample_factor: image down sample factor for optimizing processing time
:type down_sample_factor: int
:return: list of candidate crop rectangles as per input aspect ratio
:rtype: list of CropRect
"""
crop_list_tuple, crop_list = [], []
# Calculate height ratio and width ratio of input and target image
height_ratio, width_ratio = (
input_image_height / target_height,
input_image_width / target_width,
)
# Generate candidate crops, _get_crop_spec function changes it's behavior based
# on whether height_ratio is greater or smaller than width ratio.
if height_ratio <= width_ratio:
crop_list_tuple += self._get_crop_specs(
input_image_height,
input_image_width,
target_height,
target_width,
is_height_small=True,
)
else: # elif width_ratio < height_ratio:
crop_list_tuple += self._get_crop_specs(
input_image_height,
input_image_width,
target_height,
target_width,
is_height_small=False,
)
# For each of crop_specifications generated by _get_crop_spec() function
# generate actual crop as well as give score to each of these crop
for crop_height, crop_width in crop_list_tuple:
crop_list += self.crop_image_from_cvimage(
input_image=imgFile,
crop_width=crop_width,
crop_height=crop_height,
num_of_crops=num_of_crops,
filters=filters,
down_sample_factor=down_sample_factor,
)
return crop_list
[docs] @FileDecorators.validate_dir_path
def resize_image_from_dir(
self,
dir_path,
target_width,
target_height,
down_sample_factor=config.Image.down_sample_factor,
):
"""smartly resizes all the images (inside a directory) based on the specification - width and height
:param dir_path: Input Directory path
:type dir_path: str, required
:param target_width: output width
:type target_width: int
:param target_height: output height
:type target_height: int
:param down_sample_factor: number by which you want to reduce image height & width (use it if image is large or to fasten the process)
:type down_sample_factor: int [default=8]
:return: dict with key as filepath and resized image as in opencv format as value
:rtype: dict
"""
all_resized_images = {}
for path, subdirs, files in os.walk(dir_path):
for filename in files:
filepath = os.path.join(path, filename)
image_file_path = os.path.join(path, filename)
if self._check_if_valid_image(image_file_path):
resized_image = self.resize_image(
image_file_path, target_width, target_height, down_sample_factor
)
all_resized_images[filepath] = resized_image
else:
print("Error: Not a valid image file:", image_file_path)
return all_resized_images
[docs] @FileDecorators.validate_file_path
def save_image_to_disk(self, image, file_path, file_name, file_ext):
"""saves an in-memory image obtained from image resize on drive.
:param image: In-memory input image.
:type image: numpy.ndarray, required
:param file_name: name of the image.
:type file_name: str, required
:param file_path: Folder location where files needs to be saved
:type file_path: str, required
:param file_ext: File extension indicating the file type for example - '.jpg'
:type file_ext: str, required
:return: None
"""
file_full_path = os.path.join(file_path, file_name + file_ext)
cv2.imwrite(file_full_path, image)
@FileDecorators.validate_file_path
def _check_if_valid_image(self, file_path):
"""Function to check if given image file is a valid image compatible with
opencv
:param file_path: image filename
:type file_path: str
:return: Return True if valid image file else False
:rtype: bool
"""
try:
frame = cv2.imread(file_path)
# Making sure video frame is not empty
if frame is not None:
return True
else:
return False
except cv2.error as e:
print("cv2.error:", e)
return False
except Exception as e:
print("Exception:", e)
return False