Skip to content

Instantly share code, notes, and snippets.

@rmacqueen
Created May 15, 2014 22:32
Show Gist options
  • Save rmacqueen/9ec6e00d92f2502fca92 to your computer and use it in GitHub Desktop.
Save rmacqueen/9ec6e00d92f2502fca92 to your computer and use it in GitHub Desktop.
import collections
import Image
import ImageFilter
import util
from resource_prefixes import *
import image_processing.image_tools as ImageTools
class PhotosModel(object):
"""
The model for the photo being edited. Uses the Python Imaging Library to
modify the current open photo.
"""
def __init__(self, displayable=True):
super(PhotosModel, self).__init__()
self._source_image = None
self._cropped_image = None
self._filtered_image = None
self._blurred_image = None
self._distorted_image = None
self._adjusted_image = None
self._rotated_image = None
self._options_stored = False
self._displayable = displayable
if displayable:
# Only import this if displayable; we don't want to do any graphical
# operations in generate_filter_thumbnails.py
from photos_image_widget import PhotosImageWidget
self._image_widget = PhotosImageWidget()
else:
self._image_widget = None
self._is_saved = True
self._build_filter_dict()
self._build_border_dict()
self._build_blur_dict()
self._build_distortions_dict()
def rotate_orientation_clockwise(self):
self._orientation = (self._orientation + 90) % 360
def rotate_orientation_counter_clockwise(self):
self._orientation = (self._orientation - 90) % 360
def rotate_image_by_orientation(self, img, orientation):
c_rot = lambda im: ImageTools.rotate_clockwise(im)
cc_rot = lambda im: ImageTools.rotate_counter_clockwise(im)
if orientation == 0:
f = lambda im: im
if orientation == 90 or orientation == -270:
f = c_rot
if orientation == 180 or orientation == -180:
f = lambda im: c_rot(c_rot(im))
if orientation == 270 or orientation == -90:
f = cc_rot
return f(img)
def _build_blur_dict(self):
self._blur_dict = collections.OrderedDict([
(_("None"), lambda im: im),
(_("Tilt Shift"), lambda im: ImageTools.tilt_shift_blur(im)),
(_("Depth of Field"), lambda im: ImageTools.depth_of_field_blur(im))
])
def _build_filter_dict(self):
self._filter_dict = collections.OrderedDict([
(_("None"), lambda im: im),
(_("Grayscale"), lambda im: ImageTools.grayscale(im)),
(_("Country"), lambda im: ImageTools.country(im)),
(_("Grungify"), lambda im: ImageTools.grunge(im)),
(_("Old Photo"), lambda im: ImageTools.old_photo(im)),
(_("Colorful"), lambda im: ImageTools.colorful(im)),
# (_("BUMPY"), lambda im: ImageTools.bumpy(im)),
(_("Lomo"), lambda im: ImageTools.lumo(im)),
# (_("SMOOTH"), lambda im: im.filter(ImageFilter.SMOOTH_MORE)),
# (_("SHARPEN"), lambda im: im.filter(ImageFilter.SHARPEN)),
(_("Foggy Blue"), lambda im: ImageTools.foggy_blue(im)),
(_("Paper"), lambda im: ImageTools.paper(im)),
(_("Trains"), lambda im: ImageTools.trains(im)),
(_("Desert"), lambda im: ImageTools.desert(im)),
(_("Posterize"), lambda im: ImageTools.posterize(im)),
# (_("INVERT"), lambda im: ImageTools.invert(im)),
# (_("EMBOSS"), lambda im: im.filter(ImageFilter.EMBOSS)),
(_("Edges"), lambda im: im.filter(ImageFilter.FIND_EDGES)),
# (_("PIXELATE"), lambda im: ImageTools.pixelate(im)),
# (_("BOXELATE"), lambda im: ImageTools.boxelate(im)),
])
def _build_border_dict(self):
self._border_dict = collections.OrderedDict([
(_("None"), None),
# (_("HORIZONTAL BARS"), "horizontal_bars.png"),
# (_("SIDE BARS"), "vertical_bars.png"),
(_("Crayon"), "frame_3x2_crayon.png"),
(_("Grunge"), "frame_3x2_grunge.png"),
(_("Spray"), "frame_3x2_spray.png"),
(_("Brush"), "frame_3x2_brush.png")
])
def _build_distortions_dict(self):
self._distortions_dict = collections.OrderedDict([
(_("None"), lambda im: im),
(_("Fish Eye Light"), lambda im: ImageTools.distortion(im, "FISH EYE LIGHT")),
(_("Fish Eye Heavy"), lambda im: ImageTools.distortion(im, "FISH EYE HEAVY")),
(_("Pinch Light"), lambda im: ImageTools.distortion(im, "PINCH LIGHT")),
(_("Pinch Heavy"), lambda im: ImageTools.distortion(im, "PINCH HEAVY")),
(_("Swirl"), lambda im: ImageTools.distortion(im, "SWIRL"))
])
# Store options temporarily if nothing has already been stored, then
# clear all settings (except orientation) to default in preparation for cropping
def push_options(self):
if not self._options_stored:
self._stored_filter = self._filter
self._stored_distort = self._distort
self._stored_blur_type = self._blur_type
self._stored_brightness = self._brightness
self._stored_contrast = self._contrast
self._stored_saturation = self._saturation
self._stored_orientation = self._orientation
self._stored_border = self._border
self._stored_last_crop_coordinates = self._last_crop_coordinates
self._stored_crop_orientation = self._crop_orientation
self._options_stored = True
self.clear_options()
self._orientation = self._stored_orientation
self._crop_orientation = self._stored_crop_orientation
# Restore all settings after cropping either performed or cancelled (if
# any have been stored at all)
def pop_options(self):
if self._options_stored:
self._filter = self._stored_filter
self._distort = self._stored_distort
self._blur_type = self._stored_blur_type
self._brightness = self._stored_brightness
self._contrast = self._stored_contrast
self._border = self._stored_border
self._last_crop_coordinates = self._stored_last_crop_coordinates
self._saturation = self._stored_saturation
self._options_stored = False
def clear_options(self):
self._filter = self._get_default_filter()
self._distort = self._get_default_distortion()
self._blur_type = self._get_default_blur()
self._orientation = 0
self._crop_orientation = 0
self._brightness = 1.0
self._crop_coordinates = None
self._contrast = 1.0
self._saturation = 1.0
self._last_orientation = 0
self._last_crop_coordinates = None
self._last_filter = ""
self._last_blur_type = ""
self._last_distort = ""
self._last_brightness = self._last_contrast = self._last_saturation = -1
self._border = self._get_default_border()
def revert_to_original(self):
if self._displayable:
self._image_widget.hide_crop_overlay()
self._image_widget.reset_crop_overlay()
self.clear_options()
if self.is_open():
self._update_base_image()
self._update_border_image()
self._is_saved = True
def _get_default_filter(self):
return self._filter_dict.keys()[0]
def _get_default_border(self):
return self._border_dict.keys()[0]
def _get_default_distortion(self):
return self._distortions_dict.keys()[0]
def _get_default_blur(self):
return self._blur_dict.keys()[0]
def get_image_widget(self):
return self._image_widget
def open(self, filename):
self._filename = filename
self._source_image = ImageTools.limit_size(Image.open(filename), (2056, 2056)).convert('RGB')
self.revert_to_original()
# format, an image format string will be inferred from filename by default
# quality, the quality of the image (100% is max) for lossy image formats
# save_point, whether to consider this a save point (i.e. the user triggered this save)
def save(self, filename, format=None, quality=95, save_point=False):
if self.is_open():
im = self._composite_final_image()
if format is not None:
im.save(filename, format, quality=quality)
else:
im.save(filename, quality=quality)
if save_point:
self._is_saved = True
def is_open(self):
return self._source_image is not None
def is_saved(self):
return self._is_saved
def is_modified(self):
return self._curr_filter is not self.get_defualt_filter_name()
def get_current_filename(self):
if not self.is_open():
return None
return self._filename
def get_blur_names(self):
return self._blur_dict.keys()
def get_blur_names_and_thumbnails(self):
names_and_thumbs = []
blur_no = 0
for name in self._blur_dict.keys():
names_and_thumbs.append((name, "blur_" + str(blur_no) + ".jpg"))
blur_no += 1
return names_and_thumbs
def get_filter_names(self):
return self._filter_dict.keys()
def get_filter_names_and_thumbnails(self):
names_and_thumbs = []
filter_no = 0
for name in self._filter_dict.keys():
names_and_thumbs.append((name, "filter_" + str(filter_no) + ".jpg"))
filter_no += 1
return names_and_thumbs
def get_border_names(self):
return self._border_dict.keys()
def get_border_names_and_thumbnails(self):
names_and_thumbs = []
border_no = 0
for name in self._border_dict.keys():
names_and_thumbs.append((name, "border_" + str(border_no) + ".jpg"))
border_no += 1
return names_and_thumbs
def get_distortion_names(self):
return self._distortions_dict.keys()
def get_distortion_names_and_thumbnails(self):
names_and_thumbs = []
distort_no = 0
for name in self._distortions_dict.keys():
names_and_thumbs.append((name, "distort_" + str(distort_no) + ".jpg"))
distort_no += 1
return names_and_thumbs
def get_contrast(self):
return self._contrast
def set_contrast(self, value):
self._contrast = value
self._update_base_image()
def get_brightness(self):
return self._brightness
def do_rotate(self):
self.rotate_orientation_clockwise()
self._image_widget.rotate_crop_overlay()
self._update_base_image()
self._update_border_image()
def is_crop_active(self):
return self._image_widget.crop_overlay_visible
def do_crop_activate(self):
# Crop overlay about to become visible, so resize it to the last crop selection.
# General strategy here is to set the cropbox's dimensions to the (potentially rotated)
# _last_crop_coordinates, and then perform the crop overlay's "rotate" operation as
# many times as is necessary to orient those dimensions to the current orientation.
width, height = self._source_image.size
# if the crop was made when the image was on its side, swap height and width
if self._crop_orientation in (90, 270):
width, height = height, width
self._image_widget.set_crop_selection(self._last_crop_coordinates, width, height)
# target_orientation = (degrees needed to rotate _crop_orientation to 0deg) + (current orientation)
un_orient_crop = 360 - self._crop_orientation
target_orientation = (un_orient_crop + self._orientation) % 360
# crop overlay's rotation operation works at 90 degree intervals (clockwise)
num_rotations = target_orientation / 90
for times in range(0, num_rotations):
self._image_widget.rotate_crop_overlay()
# Reset the crop coordinates, dropping any previous croppings
self._crop_coordinates = None
# Store the existing image settings elsewhere, and reset current
# settings to default so the image under the crop overlay is
# the original one
self.push_options()
# Make sure the crop overlay is showing
self._image_widget.show_crop_overlay()
self._update_base_image()
self._update_border_image()
def do_crop_apply(self):
# Get the coordinates of the crop overlay square. If the orientation
# is such that the image is on its side, transpose the height/width
# to reflect this
width, height = self._source_image.size
if self._orientation in (90, 270):
width, height = height, width
self.pop_options()
self._last_crop_coordinates = None
self._crop_coordinates = self._image_widget.get_crop_selection(width, height)
self._crop_orientation = self._orientation
self._image_widget.hide_crop_overlay()
self._update_base_image()
self._update_border_image()
def do_crop_cancel(self):
# Reclaim all effects which were applied before cropping began,
# including crop coordinates (if any)
self.pop_options()
self._image_widget.hide_crop_overlay()
# Revert the base image's crop state to what it was before
# the crop overlay was activated
self._update_base_image(revert_crop=True)
self._update_border_image()
def set_blur(self, value):
self._blur_type = value
self._update_base_image()
def get_blur(self):
return self._blur_type
def set_brightness(self, value):
self._brightness = value
self._update_base_image()
def get_saturation(self):
return self._saturation
def set_saturation(self, value):
self._saturation = value
self._update_base_image()
def get_filter(self):
return self._filter
def set_filter(self, filter_name):
self._filter = filter_name
self._update_base_image()
def get_border(self):
return self._border
def set_border(self, border_name):
if (not self.is_open()):
return
self._border = border_name
self._update_border_image()
def set_distortion(self, distort_name):
self._distort = distort_name
self._update_base_image()
def get_distortion(self):
return self._distort
def _update_base_image(self, revert_crop=False):
if (not self.is_open()):
return
modified = False
if self._crop_coordinates == None:
self._cropped_image = self._source_image
if self._orientation == 0:
self._rotated_image = self._cropped_image
# If we need to crop, first rotate the image, then apply the crop, and then
# rotate the cropped image back to 0 degrees. The image must first be rotated
# because the coordinates are oriented relative to the Clutter widget, not
# the image. The image is then rotated back to default orientation so that
# rotations on the cropped image are 1:1 with rotations on the base image
if self._crop_coordinates not in (None, self._last_crop_coordinates) or revert_crop:
if revert_crop:
orientation_when_cropped = self._crop_orientation
self._crop_coordinates = self._last_crop_coordinates
else:
orientation_when_cropped = self._orientation
self._last_crop_coordinates = self._crop_coordinates
modified = True
temp_rotated = ImageTools.rotate_by_angle(self._source_image, orientation_when_cropped)
rotated_cropped = temp_rotated.crop(self._crop_coordinates)
self._cropped_image = ImageTools.rotate_by_angle(rotated_cropped, -orientation_when_cropped)
if not self._last_orientation == self._orientation or modified:
modified = True
self._rotated_image = ImageTools.rotate_by_angle(self._cropped_image, self._orientation)
self._last_orientation = self._orientation
# filter
if not self._filter == self._last_filter or modified:
if self._filter in self._filter_dict:
modified = True
self._last_filter = self._filter
self._filtered_image = self._filter_dict[self._filter](self._rotated_image)
else:
print "Filter not supported!"
# distort
if not self._distort == self._last_distort or modified:
modified = True
self._last_distort = self._distort
self._distorted_image = self._distortions_dict[self._distort](self._filtered_image)
# blur
if not self._last_blur_type == self._blur_type or modified:
if self._blur_type in self._blur_dict:
modified = True
self._last_blur_type = self._blur_type
self._blurred_image = self._blur_dict[self._blur_type](self._distorted_image)
else:
print "Blur not supported!"
# adjust
adjusted = not (self._last_brightness == self._brightness
and self._last_contrast == self._contrast
and self._last_saturation == self._saturation)
if adjusted or modified:
modified = True
im = ImageTools.apply_contrast(self._blurred_image, self._contrast)
im = ImageTools.apply_brightness(im, self._brightness)
im = ImageTools.apply_saturation(im, self._saturation)
self._adjusted_image = im
# update widget
if modified:
width, height = self._adjusted_image.size
if self._displayable:
self._image_widget.replace_base_image(
self._adjusted_image.tostring(), width, height)
self._is_saved = False
def _update_border_image(self):
filename = self._border_dict[self._border]
if filename is not None:
border = util.load_pil_image_from_resource(BORDERS_RESOURCE_PREFIX + filename)
self._border_image = border.resize(self._adjusted_image.size, Image.BILINEAR)
width, height = self._border_image.size
if self._displayable:
self._image_widget.replace_border_image(
self._border_image.tostring(), width, height)
self._is_saved = False
else:
self._border_image = None
if self._displayable:
self._image_widget.hide_border_image()
def _composite_final_image(self):
if self._border_image is None:
return self._adjusted_image
return Image.composite(self._border_image, self._adjusted_image, self._border_image)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment