6
\$\begingroup\$

I wrote a simple GUI applications to help me select 'positive' regions of a bunch of photos for the purpose of training an object detectir using OpenCV Haar Cascades.

For training purposes, you need a series of 'positive' images which show the kind of item you want, closely cropped, and then you need a bunch of negative/background images that don't have the item at all. This script helps you generate both at once by paging through photos and quickly generating the rectangle you want to use to crop the image to select the 'positive' region.

I'd like feedback on everything, of course, but particularly on whether the division of labor is sensible and also on formatting. I have the feeling I could make this code a lot more readable.

imageeditor.py

import Tkinter
import Image, ImageTk
from Tkinter import Tk, BOTH
from ttk import Frame, Button, Style

import imagefeed
import imagefilefeed

IMAGE_DIRECTORY = "/home/whales/imgs/"
POSITIVE_DIRECTORY = "/home/whales/tkinter/positive/"
NEGATIVE_DIRECTORY = "/home/whales/tkinter/negative/"
IMAGE_RESIZE_FACTOR = .2


class ImageEditor(Frame):
    """class ImageEditor provides functionality to page through photos so that users can  select a portion 
   of each photo to be saved separately as a 'positive' image and the rest to be saved separately as
   four 'negative' images"""

    def __init__(self, parent):
        """Initializes the window with access to an imagefeed class that supplies from and saves images to the appropriate locations"""
        Frame.__init__(self, parent)           
        self.parent = parent
        self.corners = []
        self.image_feed = imagefeed.ImageFeed(imagefilefeed.FileFeed(IMAGE_DIRECTORY, POSITIVE_DIRECTORY, NEGATIVE_DIRECTORY), IMAGE_RESIZE_FACTOR)
        self.image = self.image_feed.returnTKImage()
        self.canvas = None
        self.initUI()
        self.resetCanvas()


    def initUI(self):
        """Adds a Tkinter canvas element that tracks mouse clicks to select image region for saving"""
        self.style = Style()
        self.style.theme_use("default")
        self.pack(fill=BOTH, expand=1)

        self.canvas = Tkinter.Canvas(self, width = self.image.width(), height = self.image.height())       
        self.canvas.bind("<Button-1>", self.OnMouseDown)
        self.canvas.pack()

        nextButton = Button(self, text="Next", command=self.next)
        nextButton.place(x=0, y=0)

        resetButton = Button(self, text="Reset", command=self.reset)
        resetButton.place(x=0, y=22)


    def next(self):
        """Saves current edits and advances to the next image"""
        if len(self.corners) == 2:
            self.image_feed.writeImages(self.corners)
        self.image_feed.nextImage()
        self.reset()    


    def resetCanvas(self):
        """Resets all canvas elements without advancing forward"""
        self.image = self.image_feed.returnTKImage()
        self.canvas.create_image(0, 0, image=self.image, anchor="nw")
        self.canvas.configure(height = self.image.height(), width = self.image.width())
        self.canvas.place(x = 0, y = 0, height = self.image.height(), width = self.image.width())


    def reset(self):
        """Removes all drawings on the canvas so user can start over on same image"""
        self.corners = []
        self.canvas.delete("all")
        self.resetCanvas()


    def OnMouseDown(self, event):
        """Records location of user clicks to establish cropping region"""
        self.corners.append([event.x, event.y])
        if len(self.corners) == 2:
            self.canvas.create_rectangle(self.corners[0][0], self.corners[0][1], self.corners[1][0], self.corners[1][1], outline ='cyan', width = 2)


def main():
    root = Tk()
    root.geometry("250x150+300+300")
    app = ImageEditor(root)
    root.mainloop()  


if __name__ == '__main__':
    main()  

imagefeed.py

import cv2
import Image, ImageTk

class ImageFeed:
    """ The ImageFeed class manages all operations related to loading, saving, and formatting 
        images for presentation. The ImageFeed has a member file manager that determines what
        files it loads. The ImageFeed supplies a TkinterImage to requester objects."""

    def __init__(self, file_feed, rescale_factor):
        self.file_feed = file_feed
        self.rescale_factor = rescale_factor
        self.image = None
        self.cv_img = None
        self.nextImage()


    def returnTKImage(self):
        return self.image


    def nextImage(self):
        """ Calls the file feed's method to advance in the file list and then loads and formats
            the next image file."""
        img = cv2.imread(self.file_feed.next_file())            
        self.cv_img = img
        img_small = cv2.resize(img, (0,0), fx = self.rescale_factor, fy = self.rescale_factor)
        b, g, r = cv2.split(img_small)
        img_small = cv2.merge((r,g,b))
        im = Image.fromarray(img_small)
        self.image = ImageTk.PhotoImage(image=im)       


    def writeImages(self, corners):
        """ Writes the single 'positive' image to the positive directory and the four 'negative' images to the negative directory.
            The 'negative' images are the four rectangles around the positive image that do not contain the positive image. The 
            parameter corners supplies two diagonal points of the rectangle enclosing the 'positive' region of the image."""
        new_img = self.cv_img[corners[0][1]/self.rescale_factor:corners[1][1]/self.rescale_factor, corners[0][0]/self.rescale_factor:corners[1][0]/self.rescale_factor]
        cv2.imwrite("".join(self.file_feed.get_positive_file()), new_img)

        low_x = min(corners[0][0], corners[1][0])/self.rescale_factor
        high_x = max(corners[0][0], corners[1][0])/self.rescale_factor
        low_y = min(corners[0][1], corners[1][1])/self.rescale_factor
        high_y = max(corners[0][1], corners[1][1])/self.rescale_factor
        neg_file_name = self.file_feed.get_negative_file();

        new_img = self.cv_img[ :low_y, :]
        cv2.imwrite("{}{}{}".format(neg_file_name[0], "LY", neg_file_name[1]), new_img)
        new_img = self.cv_img[ high_y: , :]
        cv2.imwrite("{}{}{}".format(neg_file_name[0], "HY", neg_file_name[1]), new_img)

        new_img = self.cv_img[ :, :low_x ]
        cv2.imwrite("{}{}{}".format(neg_file_name[0], "LX", neg_file_name[1]), new_img)
        new_img = self.cv_img[:,  high_x: ]
        cv2.imwrite("{}{}{}".format(neg_file_name[0], "HX", neg_file_name[1]), new_img)

imagefilefeed.py

import os

class FileFeed():
    """The FileFeed class determines appropriate file paths to use for retrieving images
       and for saving 'positive' and 'negative' images while avoiding duplicating work
       in the event that user completes classification in multiple sessions. To avoid duplication
       the same positive_directory and negative_directory should always be used for one batch"""

    def __init__(self, existing_directory, positive_directory, negative_directory):
        """Saves file locations to instance variables and determines the appropriate files 
           for editing, based on removing any that have already been edited"""

        self.existing_directory = existing_directory
        self.positive_directory = positive_directory
        self.negative_directory = negative_directory
        self.index = 0

        # Retrieves complete list of files to be edited
        list_of_files = []
        file_names = []
        walker = iter(os.walk(self.existing_directory))
        next(walker)
        for dir, _, _ in walker:
            files = [dir + "/" +  file for file in os.listdir(dir)]            
            list_of_files.extend(files)
            file_names.extend(os.listdir(dir))

        # Determines which files have already been edited
        list_of_processed_files = []
        processed_file_names = []
        walker = iter(os.walk(self.positive_directory))
        next(walker)
        for dir, _, _ in walker:
            files = [dir + "/" +  file for file in os.listdir(dir)]            
            list_of_processed_files.extend(files)
            processed_file_names.extend(os.listdir(dir))

        # List of files to edit does not include those that have already been edited
        good_names = set(file_names) - set(processed_file_names)
        self.list_of_files = [f for i, f in enumerate(list_of_files) if file_names[i] in good_names] 


    def next_file(self):
        self.index += 1
        return self.list_of_files[self.index - 1]


    def get_negative_file(self):
        """Returns a tuple containing the absolute directory (0) and filename (1)
        where a negative file should be saved for the corresponding image/index. This 
        preserves folder and file identity from the original directory, but now in the
        'negative' directory."""
        files = self.list_of_files[self.index].split("/")
        try:
            os.stat(self.negative_directory+files[-2])
        except:
            os.mkdir(self.negative_directory+files[-2])
        return ("{}{}/".format(self.negative_directory, files[-2]), files[-1])


    def get_positive_file(self):
        """Returns same items as get_negative_file except for the positive directory"""
        files = self.list_of_files[self.index].split("/")
        try:
            os.stat(self.positive_directory+files[-2])
        except:
            os.mkdir(self.positive_directory+files[-2])
        return ("{}{}/".format(self.positive_directory, files[-2]), files[-1])
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

For improving readability and general coding style, PEP0008 is invaluable. It's the Python style guide and it details a lot of useful conventions to help with this. But there's two in particular I'll advise you about.

Docstrings

It's good to include docstrings, but you have it formatted a bit off and rendundantly. To take your ImageEditor class for example, you don't need to begin with class ImageEditor, anyway reading it already knows which class it's from. If you're going to have multiple lines, you should just have a single line summarising the class, followed by a blank line and then the rest of the detail. The Python style guide recommends you stick to a maximum of 79 characters per line, so you should keep to that too. Here's how I'd rewrite your docstring:

class ImageEditor(Frame):
    """Pages through photos so users can edit them.

   Allows a user to select a portion of each photo to be saved separately
   as a 'positive' image and the rest to be saved separately as
   four 'negative' images."""

There's a lot more detail in the PEP about docstring conventions. And you can probably rewrite that better than me as the author of the code.

Character limit

The character limit I mentioned above also comes in to play in your actual code. You might be averse to splitting long lines up because of how Python cares about whitespace, but you can freely split up lines in certain cases, like when you're using brackets ().

    self.image_feed = imagefeed.ImageFeed(imagefilefeed.FileFeed(IMAGE_DIRECTORY, POSITIVE_DIRECTORY, NEGATIVE_DIRECTORY), IMAGE_RESIZE_FACTOR)

You could split that line up inside the brackets to make it look like this:

    self.image_feed = imagefeed.ImageFeed(
                        imagefilefeed.FileFeed(
                            IMAGE_DIRECTORY, POSITIVE_DIRECTORY, NEGATIVE_DIRECTORY),
                        IMAGE_RESIZE_FACTOR)

Your personal style may vary but I tried to indent it to show that the imagefilefeed is part of the same set of parameters as IMAGE_RESIZE_FACTOR.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.