As my work is predominantly focused within Maya this means that I do not have a lot of experience of creating standalone tools for use outside a Maya environment.
These days Maya has everything you need to write decent tools. Python is embedded along with PySide for PyQt and now that the API has support for Python this has made it more easy to write complex tooling compared to a purely Mel and C++ offering of the bad old days.
However writing tooling outside is a different beast as none of this stuff comes pre-installed.
ImageCompare Tool
Python and PyQt
Python Interpreter: Python 2.7 64 bit
PyQt: PyQt for 2.7 64 bit exe
When I first approached the most confusing thing was looking at all the different versions of the software and libraries there were and working out which one was applicable. 64 Bit or 32 Bit? 2.7, 3.3, source, binary, exe, tar...blah, blah, blah. After some false starts I found a good combination that worked well for me although I guess that depending on requirements you may need different versions of the software.
The links above give you enough to get you up and writing tools without much hassle and as all of the above are installers they will be setup automatically skipping the more complicated requirements of the PyQt manual install that includes fiddling with sip.
Python Imaging Library
Python Imaging Library: PIL 64 bit exe
As this tool was intended for finding duplicate images I needed an extra library referred to as Pil, Python imaging library.
I wanted to be able to open an image and then provided it's dimensions matched the source return the pixel RGB values based on a sample rate, for instance every tenth pixel. As long as the values match then the compare continues until the end of the image is reached. If there is no disparity a match has been found. PIL gives all of this and more and seemed to be incredibly quick at pixel sampling
Py2exe
Compile your python: Py2exe 64 bit
To test it rather than running it over and over in the Python environment I opted to convert it to an executable using py2exe. It was quick to convert the Python code into a tool that could be run with a simple double click. The only drawback of this method was that when there was a fault in my code the error was lost as the window would close before it could be read. In the end I had to create a little batch file to run the executable with a pause at the end to allow me to read each problem as it appeared.
To work with py2exe you will need a setup.py file. This is used by py2exe and gives it basic instructions about how to compile your py file(s). I found that to run my main program I needed to call it in separate py file. This is linked to the setup.py file so that when compiling py2exe will make sure that your program is run correctly based on what is in this file. Py2exe also sources all of the libraries you are using and includes them with the executable.
In addition you might consider using the batch file mentioned earlier to actually run your program whilst you are iterating and testing. This way you can catch any errors that occur.
These files are placed in a relevant location to the Python folder. In my case I placed them directly at the root of the Python27 folder.
If successfully compiled the executable will be placed into a 'dist' folder along with other libraries that the program requires.
One other thing to note is that if you wish to add an icon to your new application then all you need do is specify the filename after the 'icon_resources' key contained within the setup.py file. The caveat here is that it appears the setup needs to run twice to properly embed the icon. This is probably a bug or simply perhaps something I have missed. If run twice this obviously doubles the length of compilation time.
Check out the video below to see what I have so far and then below that is the source code for the application.
ImageCompare: Python, PyQt, PIL and py2exe from SBGrover on Vimeo.
Try it out!!
Below I include the files I have created for my application simply to give you an idea of the setup and to have something to try out.
1. Write your code
ImageCompare.py
# Import the modules
import sys
from PyQt4 import QtCore, QtGui
from functools import partial
import Image
import os
import subprocess
class VerticalWidget(QtGui.QWidget):
def __init__(self):
super(VerticalWidget, self).__init__()
self.layout = QtGui.QVBoxLayout(self)
self.setLayout(self.layout)
class HorizontalWidget(QtGui.QHBoxLayout):
def __init__(self, layout):
super(HorizontalWidget, self).__init__()
layout.addLayout(self, QtCore.Qt.AlignLeft)
class MainButtonWidget(QtGui.QPushButton):
def __init__(self, layout, name, main_object, command, width):
super(MainButtonWidget, self).__init__()
layout.addWidget(self, QtCore.Qt.AlignLeft)
self.setMaximumWidth(width)
self.setMaximumHeight(24)
self.setText(name)
self.main_object = main_object
self.command = command
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.run_command(self.command)
def run_command(self, command):
exec command
class TabLayout(QtGui.QTabWidget):
def __init__(self, tab_dict):
super(TabLayout, self).__init__()
for tab in tab_dict:
self.addTab(tab[0], tab[1])
class OutputView(QtGui.QListWidget):
def __init__(self, layout):
super(OutputView, self).__init__()
layout.addWidget(self, QtCore.Qt.AlignLeft)
self.setSelectionMode(0)
self.setUpdatesEnabled(True)
class ListView(QtGui.QListWidget):
def __init__(self, layout):
super(ListView, self).__init__()
layout.addWidget(self, QtCore.Qt.AlignLeft)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.connect(self, QtCore.SIGNAL("customContextMenuRequested(QPoint)" ), self.rightClicked)
def rightClicked(self, QPos):
self.listMenu = QtGui.QMenu()
menu_item_a = QtGui.QAction("Open in Explorer", self.listMenu)
self.listMenu.addAction(menu_item_a)
menu_item_a.triggered.connect(self.open_in_explorer)
menu_item_b = QtGui.QAction("Delete File", self.listMenu)
self.listMenu.addAction(menu_item_b)
menu_item_b.triggered.connect(self.delete_file)
menu_item_c = QtGui.QAction("Open File", self.listMenu)
self.listMenu.addAction(menu_item_c)
menu_item_c.triggered.connect(self.open_file)
parentPosition = self.mapToGlobal(QtCore.QPoint(0, 0))
self.listMenu.move(parentPosition + QPos)
self.listMenu.show()
def open_in_explorer(self):
path = self.currentItem().text()
path = path.replace("/", "\\")
subprocess.Popen('explorer /select,' + r'%s' %path)
def open_file(self):
path = self.currentItem().text()
os.startfile(str(path))
def delete_file(self):
path_list = self.selectedItems()
for path in path_list:
os.remove(str(path.text()))
self.takeItem(self.row(path))
class SpinBox(QtGui.QSpinBox):
def __init__(self, layout):
super(SpinBox, self).__init__()
layout.addWidget(self, QtCore.Qt.AlignLeft)
self.setValue(10)
self.setMaximumWidth(40)
self.setMaximum(100)
self.setMinimum(0)
class FileTextEdit(QtGui.QTextEdit):
def __init__(self, layout):
super(FileTextEdit, self).__init__()
layout.addWidget(self, QtCore.Qt.AlignLeft)
self.setMaximumHeight(24)
self.setWordWrapMode(0)
self.setHorizontalScrollBarPolicy(1)
self.setVerticalScrollBarPolicy(1)
class Text(QtGui.QLabel):
def __init__(self, layout, text):
super(Text, self).__init__()
layout.addWidget(self, QtCore.Qt.AlignRight)
self.setText(text)
self.setMaximumWidth(70)
class ImageCompare_Helpers():
def get_image(self, path):
try:
im = Image.open(path)
except:
print "Pick a VALID image file (.jpg, .gif, .tga, .bmp, .png, .tif)"
rgb_im = im.convert('RGB')
return rgb_im
def get_pixel_color(self, img, sample_size):
width, height = img.size
pixel_total = width * height
pixel_set = []
pixel_range = pixel_total / sample_size
for pixel in range(pixel_range)[0::10]:
x, y = self.convert_to_pixel_position(pixel, width)
r, g, b = img.getpixel((x, y))
pixel_set.append([r, g, b])
return pixel_set
def convert_to_pixel_position(self, val, width):
x = val % width
y = val / width
return x, y
def get_file(self, main_object):
file = QtGui.QFileDialog.getOpenFileName(None, 'Select Source File', 'c:/',
selectedFilter='*.jpg')
if file:
main_object.file_text.setText(file)
def get_folder(self, main_object):
folder = QtGui.QFileDialog.getExistingDirectory(None, 'Select Search Folder')
if folder:
main_object.folder_text.setText(folder)
def do_it(self, main_object):
#main_object.main_widget.close()
#main_object.app.quit()
output_view = main_object.output_view
output_view.clear()
main_object.list_view.clear()
directory = main_object.folder_text.toPlainText()
filename = main_object.file_text.toPlainText()
if filename and directory:
matching_images = []
self.sample_size = main_object.samples.value()
self.accuracy = main_object.threshold.value()
self.accuracy = abs(self.accuracy - 100)
img1 = self.get_image(str(filename))
img1_pixels = self.get_pixel_color(img1, self.sample_size)
all_images = self.read_all_subfolders(str(directory))
width, height = img1.size
img1_size = width * height
output_view.addItem("##################")
output_view.addItem("SOURCE IMAGE ----> " + str(filename))
output_view.addItem("")
for image in all_images:
output_view.scrollToBottom()
main_object.app.processEvents()
image = image.replace("\\", "/")
if image != filename:
output_view.addItem("Comparing: " + image)
img2 = self.get_image(image)
img2_width, img2_height = img2.size
if img2_width * img2_height == img1_size:
img2_pixels = self.get_pixel_color(img2, self.sample_size)
same = self.compare_images(img1_pixels, img2_pixels)
if same:
matching_images.append(image)
output_view.addItem("")
output_view.addItem("##################")
output_view.addItem("MATCHING IMAGES")
if matching_images:
for i in matching_images:
output_view.addItem(i)
main_object.list_view.addItem(i)
else:
output_view.addItem("NONE")
output_view.scrollToBottom()
def compare_images(self, img1_pixel_set, img2_pixel_set):
length = len(img1_pixel_set)
for count, i in enumerate(img1_pixel_set):
img1_total = i[0] + i[1] + i[2]
img2_total = img2_pixel_set[count][0] + img2_pixel_set[count][1] + img2_pixel_set[count][2]
img2_upper = img2_total + self.accuracy
img2_lower = img2_total - self.accuracy
if img2_lower <= img1_total <= img2_upper:
if count == length - 1:
return 1
else:
return 0
break
def read_all_subfolders(self, path):
all_images = []
suffix_list = [".jpg", ".gif", ".tga", ".bmp", ".png", ".tif"]
for root, dirs, files in os.walk(path):
for file in files:
for suffix in suffix_list:
if file.lower().endswith(suffix):
all_images.append(os.path.join(root, file))
return all_images
class ImageCompare_UI(ImageCompare_Helpers):
def __init__(self):
self.app = None
self.main_widget = None
def run_ui(self, ImageCompare):
self.app = QtGui.QApplication(sys.argv)
self.main_widget = QtGui.QWidget()
self.main_widget.resize(600, 600)
main_layout = VerticalWidget()
# VIEW FILES
vertical_layout_list = VerticalWidget()
self.list_view = ListView(vertical_layout_list.layout)
#vertical_layout_list.layout.setContentsMargins(0,0,0,0)
MainButtonWidget(vertical_layout_list.layout, "Delete All", self,
"", 128)
# FIND FILES
vertical_layout = VerticalWidget()
horizontal_layout_a = HorizontalWidget(vertical_layout.layout)
Text(horizontal_layout_a, "Source File")
self.file_text = FileTextEdit(horizontal_layout_a)
MainButtonWidget(horizontal_layout_a, "<<", self,
"file = self.main_object.get_file(self.main_object)", 32)
horizontal_layout_b = HorizontalWidget(vertical_layout.layout)
Text(horizontal_layout_b, "Source Folder")
self.folder_text = FileTextEdit(horizontal_layout_b)
MainButtonWidget(horizontal_layout_b, "<<", self,
"file = self.main_object.get_folder(self.main_object)", 32)
horizontal_layout_c = HorizontalWidget(vertical_layout.layout)
Text(horizontal_layout_c, "Accuracy %")
self.threshold = SpinBox(horizontal_layout_c)
self.threshold.setValue(50)
self.threshold.setToolTip("Deviation threshold for RGB Values. Higher means more deviation but more inaccuracy")
Text(horizontal_layout_c, " Pixel Steps")
self.samples = SpinBox(horizontal_layout_c)
self.samples.setToolTip("Steps between each pixel sample. Higher is faster but less accurate")
horizontal_layout_c.addStretch()
# OUTPUT WINDOW
self.output_view = OutputView(vertical_layout.layout)
MainButtonWidget(vertical_layout.layout, "Cancel Search", self,
"", 128)
MainButtonWidget(vertical_layout.layout, "Run Image Compare", self, "self.main_object.do_it(self.main_object)", 128)
vertical_layout.layout.addStretch()
self.tab = TabLayout([[vertical_layout, "Find Files"], [vertical_layout_list, "Results"]])
main_layout.layout.addWidget(self.tab)
self.main_widget.setLayout(main_layout.layout)
self.main_widget.show()
sys.exit(self.app.exec_())
class ImageCompare(ImageCompare_UI):
def __init__(self):
print "RUNNING Compare"
QtCore.pyqtRemoveInputHook()
self.run_ui(self)
setup.py
from distutils.core import setup
from py2exe.build_exe import py2exe
setup_dict = dict(
windows = [{'script': "main.py",
"icon_resources": [(1, "image_compare.ico")], "dest_base": "ImageCompare"}],
)
setup(**setup_dict)
setup(**setup_dict)
main.py
import ImageCompare as ic
compare = ic.ImageCompare()
2. Run py2exe to compile it
In cmd type:
python setup.py py2exe
Unless python folder is in environment variables you will need to do this within the python27 folder.
3. Run a batch file to catch errors and iterate on your code
Create a new batch file that contains:
ImageCompare.exe
pause
Please share the .py file
ReplyDeleteEven if a small dot in the target photo would be discovered ???
ReplyDelete