How To Rotate Coordinates (x,y) Of An Image At A Specific Angle
Solution 1:
Although it has been long time since the question was asked. But I have decided to answer it as it has no accepted answer yet, even if it is a well accepted question. I have added a lot of comments to make the implementation clear. So, the code is hopefully self-explanatory. But I am also describing the ImageAugmentation
's parameters for further clarification:
Here, original_data_dir
is the directory to the parent folder, where all of the image's folders exists (yes it can read from multiple image folders). This parameter is compulsory.
augmentation_data_dir
is the folder directory where you want to save the outputs. The program will automatically create all sub-folders inside of the output directory just like they appear in input directory. It is totally optional, it can generate the output directory by mimicking the input directory by appending the string _augmentation
after the input folder name.
keep_original
is another optional parameter. In many cases you may want to keep the original image with the augmented images in the output folder. If you want so, make it True
(default).
num_of_augmentations_per_image
is the total number of augmented images to be generated from each image. Although you wanted only rotation, but this program is designed to do other augmentations as well, change them, add or remove them as you need. I have also added a link to documentation where you will find other augmentations which can be introduced here in this code. It is defaulted to 3
, if you keep the original image, there will be 3 + 1 = 4
images will be generated in the output.
discard_overflow_and_underflow
is for handling the case where due to spatial transformation, the augmented points along with the image underneath can go outside of image's resolution, you can optionally keep them. But it is discarded here by default. Again, it will also discard images having width
or height
values <= 0
. Defaulted to True
.
put_landmarks
means if you want the landmarks to be shown in the output. Make it True
or False
as required. It is False
by default.
Hope you like it!
import logging
import imgaug as ia
import imgaug.augmenters as iaa
from imgaug.augmentables import Keypoint
from imgaug.augmentables import KeypointsOnImage
import os
import cv2
import re
SEED = 31# To reproduce the resultclassImageAugmentation:
def__init__(self, original_data_dir, augmentation_data_dir = None, keep_original = True, num_of_augmentations_per_image = 3, discard_overflow_and_underflow = True, put_landmarks = False):
self.original_data_dir = original_data_dir
if augmentation_data_dir != None:
self.augmentation_data_dir = augmentation_data_dir
else:
self.augmentation_data_dir = self.original_data_dir + '_augmentation'# Most of the time you will want to keep the original images along with the augmented images
self.keep_original = keep_original
# For example for self.num_of_augmentations_per_image = 3, from 1 image we will get 3 more images, totaling 4 images.
self.num_of_augmentations_per_image = num_of_augmentations_per_image
# if discard_overflow_and_underflow is True, the program will discard all augmentation where landmark (and image underneath) goes outside of image resolution
self.discard_overflow_and_underflow = discard_overflow_and_underflow
# Optionally put landmarks on output images
self.put_landmarks = put_landmarks
defget_base_annotations(self):
"""This method reads all the annotation files (.txt) and make a list
of annotations to be used by other methods.
"""# base_annotations are the annotations which has come with the original images.
base_annotations = []
defget_info(content):
"""This utility function reads the content of a single annotation
file and returns the count of total number of points and a list of coordinates
of the points inside a dictionary.
As you have provided in your question, the annotation file looks like the following:
106
282.000000 292.000000
270.000000 311.000000
259.000000 330.000000
.....
.....
Here, the first line is the number of points.
The second and the following lines gives their coordinates.
"""# As all the lines newline separated, hence splitting them# accordingly first
lines = content.split('\n')
# The first line is the total count of the point, we can easily get it just by counting the points# so we are not taking this information.# From the second line to the end all lines are basically the coordinate values# of each point (in each line). So, going to each of the lines (from the second line)# and taking the coordinates as tuples.# We will end up with a list of tuples and which will be inserted to the dict "info"# under the key "point_coordinates"
points = []
for line in lines[1:]:
# Now each of the line can be splitted into two numbers representing coordinatestry:
# Keeping inside try block, as some of the lines might be accidentally contain# a single number, or it can be the case that there might be some extra newlines# where there is no number.
col, row = line.split(' ')
points.append((float(col), float(row)))
except:
pass# Returns: List of tuplesreturn points
for subdir, dirs, files in os.walk(self.original_data_dir):
for file in files:
ext = os.path.splitext(file)[-1].lower()
# Looping through image files (instead of annotation files which are in '.txt' format) # because image files can have very different extensions and we have to preserve them.# Whereas, all the annotation files are assumed to be in '.txt' format.# Annotation file's (.txt) directory will be generated from here.if ext notin ['.txt']:
input_image_file_dir = os.path.join(subdir, file)
# As the image filenames and associated annotation text filenames are the same,# so getting the common portion of them, it will be used to generate the annotation# file's directory.# Also assuming, there are no dots (.) in the input_annotation_file_dir except before the file extension.
image_annotation_base_dir = self.split_extension(input_image_file_dir)[0]
# Generating annotation file's directory
input_annotation_file_dir = image_annotation_base_dir + '.txt'try:
withopen(input_annotation_file_dir, 'r') as f:
content = f.read()
image_annotation_base_dir = os.path.splitext(input_annotation_file_dir)[0]
if os.path.isfile(input_image_file_dir):
image = cv2.imread(input_image_file_dir)
# Taking image's shape is basically surving dual purposes.# First of all, we will need the image's shape for sanity checking after augmentation# Again, if any of the input image is corrupt this following line will through exception# and we will be able to skip that corrput image.
image_shape = image.shape # height (y), width (x), channels (depth)# Collecting the directories of original annotation files and their contents.# The same folder structure will be used to save the augmented data.# As the image filenames and associated annotation text filenames are the same, so
base_annotations.append({'image_file_dir': input_image_file_dir,
'annotation_data': get_info(content = content),
'image_resolution': image_shape})
except:
logging.error(f"Unable to read the file: {input_annotation_file_dir}...SKIPPED")
return base_annotations
defget_augmentation(self, base_annotation, seed):
image_file_dir = base_annotation['image_file_dir']
image_resolution = base_annotation['image_resolution']
list_of_coordinates = base_annotation['annotation_data']
ia.seed(seed)
# We have to provide the landmarks in specific format as imgaug requires
landmarks = []
for coordinate in list_of_coordinates:
# coordinate[0] is along x axis (horizontal axis) and coordinate[1] is along y axis (vertical axis) and (left, top) corner is (0, 0)
landmarks.append(Keypoint(x = coordinate[0], y = coordinate[1]))
landmarks_on_original_img = KeypointsOnImage(landmarks, shape = image_resolution)
original_image = cv2.imread(image_file_dir)
"""
Here the magic happens. If you only want rotation then remove other transformations from here.
You can even add other various types of augmentation, see documentation here:
# Documentation for image augmentation with keypoints
https://imgaug.readthedocs.io/en/latest/source/examples_keypoints.html
# Here you will find other possible transformations
https://imgaug.readthedocs.io/en/latest/source/examples_basics.html
"""
seq = iaa.Sequential([
iaa.Affine(
scale={"x": (0.8, 1.2), "y": (0.8, 1.2)}, # scale images to 80-120% of their size, individually per axis
translate_percent={"x": (-0.2, 0.2), "y": (-0.2, 0.2)}, # translate by -20 to +20 percent (per axis)
rotate=(-90, 90), # rotate by -90 to +90 degrees; for specific angle (say 30 degree) use rotate = (30)
shear=(-16, 16), # shear by -16 to +16 degrees
)
], random_order=True) # Apply augmentations in random order
augmented_image, _landmarks_on_augmented_img = seq(image = original_image, keypoints = landmarks_on_original_img)
# Now for maintaining consistency, making the augmented landmarks to maintain same data structure like base_annotation# i.e, making it a list of tuples.
landmarks_on_augmented_img = []
for index inrange(len(landmarks_on_original_img)):
landmarks_on_augmented_img.append((_landmarks_on_augmented_img[index].x,
_landmarks_on_augmented_img[index].y))
return augmented_image, landmarks_on_augmented_img
defsplit_extension(self, path):
# Assuming there is no dots (.) except just before extension# Returns [directory_of_file_without_extension, extension]return os.path.splitext(path)
defsanity_check(self, landmarks_aug, image_resolution):
# Returns false if the landmark is outside of image resolution.# Or, if the resolution is faulty.for index inrange(len(landmarks_aug)):
if landmarks_aug[index][0] < 0or landmarks_aug[index][1] < 0:
returnFalseif landmarks_aug[index][0] >= image_resolution[1] or landmarks_aug[index][1] >= image_resolution[0]:
returnFalseif image_resolution[0] <= 0:
returnFalseif image_resolution[1] <= 0:
returnFalsereturnTruedefserialize(self, serialization_data, image):
"""This method to write the annotation file and the corresponding image.
"""# Now it is time to actually writing the image file and the annotation file!# We have to make sure the output folder exists# and "head" is the folder's directory here.
image_file_dir = serialization_data['image_file_dir']
annotation_file_dir = self.split_extension(image_file_dir)[0] + '.txt'
point_coordinates = serialization_data['annotation_data'] # List of tuples
total_points = len(point_coordinates)
# Getting the corresponding output folder for current image
head, tail = os.path.split(image_file_dir)
# Creating the folder if it doesn't existifnot os.path.isdir(head):
os.makedirs(head)
# Writing annotation filewithopen(annotation_file_dir, 'w') as f:
s = ""
s += str(total_points)
s += '\n'for point in point_coordinates:
s += "{:.6f}".format(point[0]) + ' ' + "{:6f}".format(point[1]) + '\n'
f.write(s)
if self.put_landmarks:
# Optionally put landmarks in the output images.for index inrange(total_points):
cv2.circle(image, (int(point_coordinates[index][0]), int(point_coordinates[index][1])), 2, (255, 255, 0), 2)
cv2.imwrite(image_file_dir, image)
defaugmentat_with_landmarks(self):
base_annotations = self.get_base_annotations()
for base_annotation in base_annotations:
if self.keep_original == True:
# As we are basically copying the same original data in new directory, changing the original image's directory with the new one with re.sub()
base_data = {'image_file_dir': re.sub(self.original_data_dir, self.augmentation_data_dir, base_annotation['image_file_dir']),
'annotation_data': base_annotation['annotation_data']}
self.serialize(serialization_data = base_data, image = cv2.imread(base_annotation['image_file_dir']))
for index inrange(self.num_of_augmentations_per_image):
# Getting a new augmented image in each iteration from the same base image.# Seeding (SEED) for reproducing same result across all execution in the future.# Also seed must be different for each iteration, otherwise same looking augmentation will be generated.
image_aug, landmarks_aug = self.get_augmentation(base_annotation, seed = SEED + index)
# As for spatial transformations for some images, the landmarks can go outside of the image.# So, we have to discard those cases (optionally).if self.sanity_check(landmarks_aug, base_annotation['image_resolution']) ornot self.discard_overflow_and_underflow:
# Getting the filename without extension to insert an index number in between to generate a new filename for augmented image
filepath_without_ext, ext = self.split_extension(base_annotation['image_file_dir'])
# As we are writing newly generated images to similar sub folders (just in different base directory)# that is replacing original_data_dir with augmentation_data_dir.# So, to do this we are using, re.sub(what_to_replace, with_which_to_replace, from_where_to_replace)
filepath_for_aug_img_without_ext = re.sub(self.original_data_dir, self.augmentation_data_dir, filepath_without_ext)
new_filepath_wo_ext = filepath_for_aug_img_without_ext + '_' + str(index)
augmentation_data = {
'image_file_dir': new_filepath_wo_ext + ext,
'annotation_data': landmarks_aug
}
self.serialize(serialization_data = augmentation_data, image = image_aug)
# Make put_landmarks = False if you do not want landmarks to be shown in output# original_data_dir is the single parent folder directory inside of which all image folder(s) exist.
img_aug = ImageAugmentation(original_data_dir = 'parent/folder/directory/of/img/folder', put_landmarks = True)
img_aug.augmentat_with_landmarks()
Following is a snapshot of sample output of the code:
Please note that, I have used a package imgaug
. I will suggest you to install the 0.4.0
version, as I have found it to be working. See the reason here and it's accepted answer.
Solution 2:
When you try things like that it's very important to choose the proper coordinate system. In your case you have to put the origin (0,0)
point in the center of the image.
Once you apply the rotation to the coordinates with the origin point in the center, the face points will be properly aligned on the new image.
Post a Comment for "How To Rotate Coordinates (x,y) Of An Image At A Specific Angle"