tcross-stitch.py - cross-stitch - interactively turn images into patterns for cross stitching
 (HTM) git clone git://src.adamsgaard.dk/cross-stitch
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) LICENSE
       ---
       tcross-stitch.py (9959B)
       ---
            1 #!/usr/bin/env python
            2 
            3 import sys, os
            4 import argparse
            5 import scipy.ndimage
            6 import scipy.misc
            7 import scipy.cluster
            8 import numpy
            9 import matplotlib
           10 matplotlib.use('WXAgg')
           11 import matplotlib.figure
           12 import matplotlib.backends
           13 import matplotlib.backends.backend_wxagg
           14 import matplotlib.pyplot
           15 import wx
           16 
           17 class Palette:
           18     def __init__(self, type='256colors'):
           19         if type == '256colors':
           20             self.rgblist = numpy.loadtxt('./256-color-rgb.dat')
           21 
           22     def nearest256color(self, rgbval):
           23         ibest = -1
           24         min_misfit2 = float('inf')
           25         for i in range(256):
           26             palettecolor = self.rgblist[i]
           27             misfit2 = (rgbval[0] - float(palettecolor[0]))**2 + \
           28                     (rgbval[1] - float(palettecolor[1]))**2 + \
           29                     (rgbval[2] - float(palettecolor[2]))**2
           30             if misfit2 < min_misfit2:
           31                 ibest = i
           32                 min_misfit2 = misfit2
           33         return numpy.array((self.rgblist[ibest,0], self.rgblist[ibest,1],
           34             self.rgblist[ibest,2]))
           35 
           36 
           37 class CrossStitch:
           38 
           39     def __init__(self):
           40         self.img = numpy.zeros(3)
           41 
           42     def read_image(self, infile):
           43         try:
           44             self.img = scipy.ndimage.imread(infile)
           45         except IOError:
           46             sys.stderr.write('could not open input file "' + infile + '"\n')
           47 
           48         self.orig_img = self.img.copy()
           49 
           50     def down_sample(self, width):
           51         hw_ratio = float(self.orig_img.shape[0])/self.orig_img.shape[1]
           52         size = (int(round(hw_ratio*width)), width)
           53         self.img = scipy.misc.imresize(self.orig_img, size)
           54 
           55     def limit_colors(self, ncolors):
           56         ar = self.img.reshape(scipy.product(self.img.shape[:2]),\
           57                 self.img.shape[2])
           58         self.colors, dist = scipy.cluster.vq.kmeans(ar, ncolors)
           59         tmp = ar.copy()
           60         vecs, dist = scipy.cluster.vq.vq(ar, self.colors)
           61         for i, color in enumerate(self.colors):
           62             tmp[scipy.r_[scipy.where(vecs == i)],:] = color
           63         self.img = tmp.reshape(self.img.shape[0], self.img.shape[1], 3)
           64 
           65     def convert_256_colors(self):
           66         palette = Palette('256colors')
           67         tmp = self.img.reshape(scipy.product(self.img.shape[:2]),\
           68                 self.img.shape[2])
           69         for i in range(tmp.size/3):
           70             tmp[i] = palette.nearest256color(tmp[i])
           71         self.img = tmp.reshape(self.img.shape[0], self.img.shape[1], 3)
           72 
           73     def save_image(self, filename, grid=True):
           74         fig = matplotlib.pyplot.figure()
           75         imgplot = matplotlib.pyplot.imshow(self.img, interpolation='nearest')
           76         matplotlib.pyplot.grid(grid)
           77         matplotlib.pyplot.savefig(filename)
           78 
           79     def image(self):
           80         return self.img
           81 
           82 
           83 class MainScreen(wx.Frame):
           84 
           85     def __init__(self, *args, **kwargs):
           86         super(MainScreen, self).__init__(*args, **kwargs)
           87         self.cs = CrossStitch()
           88         self.InitUI()
           89         self.contentNotSaved = False
           90         self.grid = True
           91 
           92     def InitUI(self):
           93 
           94         self.InitMenu()
           95         #self.InitToolbar()
           96         self.InitPreview()
           97 
           98         self.SetSize((600, 600))
           99         self.SetTitle('Cross Stitch')
          100         self.Centre()
          101         self.Show(True)
          102 
          103     def InitMenu(self):
          104 
          105         menubar = wx.MenuBar()
          106 
          107         fileMenu = wx.Menu()
          108         fitem = fileMenu.Append(wx.ID_OPEN, 'Open image', 'Open image')
          109         self.Bind(wx.EVT_MENU, self.OnOpen, fitem)
          110         fitem = fileMenu.Append(wx.ID_SAVE, 'Save image', 'Save image')
          111         self.Bind(wx.EVT_MENU, self.OnSave, fitem)
          112         fileMenu.AppendSeparator()
          113         fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quit application')
          114         self.Bind(wx.EVT_MENU, self.OnQuit, fitem)
          115         menubar.Append(fileMenu, '&File')
          116 
          117         processingMenu = wx.Menu()
          118         fitem = processingMenu.Append(wx.ID_ANY, 'Down sample',
          119                 'Down sample image')
          120         self.Bind(wx.EVT_MENU, self.OnDownSample, fitem)
          121         fitem = processingMenu.Append(wx.ID_ANY, 'Reduce number of colors',
          122                 'Reduce number of colors in image')
          123         self.Bind(wx.EVT_MENU, self.OnLimitColors, fitem)
          124         fitem = processingMenu.Append(wx.ID_ANY,\
          125                 'Reduce to standard 256 colors (slow)',\
          126                 'Reduce number of colors in image to the standard 256 colors')
          127         self.Bind(wx.EVT_MENU, self.On256Colors, fitem)
          128         menubar.Append(processingMenu, '&Image processing')
          129 
          130         viewMenu = wx.Menu()
          131         self.gridtoggle = viewMenu.Append(wx.ID_ANY, 'Show grid',
          132                 'Show grid in image', kind=wx.ITEM_CHECK)
          133         viewMenu.Check(self.gridtoggle.GetId(), True)
          134         self.Bind(wx.EVT_MENU, self.ToggleGrid, self.gridtoggle)
          135         menubar.Append(viewMenu, '&View')
          136 
          137         helpMenu = wx.Menu()
          138         fitem = helpMenu.Append(wx.ID_ABOUT, 'About', 'About')
          139         self.Bind(wx.EVT_MENU, self.OnAbout, fitem)
          140         menubar.Append(helpMenu, '&Help')
          141 
          142         self.SetMenuBar(menubar)
          143 
          144     def InitToolbar(self):
          145 
          146         toolbar = self.CreateToolBar()
          147         qtool = toolbar.AddLabelTool(wx.ID_EXIT, 'Quit',
          148                 wx.Bitmap('textit.png'))
          149         self.Bind(wx.EVT_TOOL, self.OnQuit, qtool)
          150 
          151         toolbar.Realize()
          152 
          153     def InitPreview(self):
          154         self.figure = matplotlib.figure.Figure()
          155         self.axes = self.figure.add_subplot(111)
          156         self.canvas = matplotlib.backends.backend_wxagg.FigureCanvasWxAgg(self,
          157                 -1, self.figure)
          158         self.sizer = wx.BoxSizer(wx.VERTICAL)
          159         self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
          160         self.SetSizer(self.sizer)
          161         self.Fit()
          162 
          163     def DrawPreview(self):
          164         self.axes.grid(self.grid)
          165         self.axes.imshow(self.cs.image(), interpolation='nearest')
          166         self.canvas.draw()
          167 
          168     def OnQuit(self, event):
          169 
          170         if self.contentNotSaved:
          171             if wx.MessageBox('Current image is not saved! Proceed?',
          172                     'Please confirm', wx.ICON_QUESTION | wx.YES_NO, self) == \
          173                     wx.NO:
          174                         return
          175         self.Close()
          176 
          177     def OnOpen(self, event):
          178         if self.contentNotSaved:
          179             if wx.MessageBox('Current image is not saved! Proceed?',
          180                     'Please confirm', wx.ICON_QUESTION | wx.YES_NO, self) == \
          181                     wx.NO:
          182                         return
          183         
          184         self.dirname = ''
          185         openFileDialog = wx.FileDialog(self, 'Open image file', self.dirname,
          186                 '', 'Image files (*.jpg, *.jpeg, *.png, *.gif, *.bmp)|'
          187                 + '*.jpg;*.jpeg;*.png;*.gif;*.bmp',
          188                 wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
          189 
          190         if openFileDialog.ShowModal() == wx.ID_OK:
          191             self.filename = openFileDialog.GetFilename()
          192             self.dirname = openFileDialog.GetDirectory()
          193             self.cs.read_image(openFileDialog.GetPath())
          194             self.DrawPreview()
          195         openFileDialog.Destroy()
          196 
          197     def OnSave(self, event):
          198         saveFileDialog = wx.FileDialog(self, 'Save image file', self.dirname,
          199                 '', 'PNG files (*.png)|*.png|'
          200                 + 'JPEG files (*.jpg,*.jpeg)|*.jpg*.jpeg|'
          201                 + 'GIF files (*.gif)|*.gif|'
          202                 + 'BMP files (*.bmp)|*.bmp',
          203                 wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
          204 
          205         if saveFileDialog.ShowModal() == wx.ID_CANCEL:
          206             return
          207 
          208         self.cs.save_image(saveFileDialog.GetPath(), grid=self.grid)
          209         self.contentNotSaved = False
          210 
          211     def OnDownSample(self, event):
          212         dlg = wx.TextEntryDialog(None, 'Enter new width', defaultValue='50')
          213         ret = dlg.ShowModal()
          214         if ret == wx.ID_OK:
          215             width = int(dlg.GetValue())
          216             self.cs.down_sample(int(width))
          217             self.contentNotSaved = True
          218             self.DrawPreview()
          219 
          220     def OnLimitColors(self, event):
          221         dlg = wx.TextEntryDialog(None, 'Enter the number of colors to include',
          222                 defaultValue='16')
          223         ret = dlg.ShowModal()
          224         if ret == wx.ID_OK:
          225             self.cs.limit_colors(int(dlg.GetValue()))
          226             self.contentNotSaved = True
          227             self.DrawPreview()
          228 
          229     def On256Colors(self, event):
          230         self.cs.convert_256_colors()
          231         self.contentNotSaved = True
          232         self.DrawPreview()
          233 
          234     def ToggleGrid(self, event):
          235         if self.gridtoggle.IsChecked():
          236             self.grid = True
          237             self.DrawPreview()
          238         else:
          239             self.grid = False
          240             self.DrawPreview()
          241 
          242     def OnAbout(self, event):
          243 
          244         description = '''Cross Stitch is a raster pattern generator for Linux,
          245 Mac OS X, and Windows. It features simple downscaling to coarsen the image
          246 resolution, and color depth reduction features.'''
          247 
          248         license = '''Cross Stitch is free software; you can redistribute it
          249 and/or modify it under the terms of the GNU General Public License as published
          250 by the Free Software Foundation; either version 3 of the License, or (at your
          251 option) any later version.
          252 
          253 Cross Stitch is distributed in the hope that it will be useful, but WITHOUT ANY
          254 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
          255 PARTICULAR PURPOSE.
          256 See the GNU General Public License for more details. You should have recieved a
          257 copy of the GNU General Public License along with Cross Stitch; if not, write to
          258 the Free Software Foundation, Inc., 59 Temple Palace, Suite 330, Boston, MA
          259 02111-1307  USA'''
          260 
          261         info = wx.AboutDialogInfo()
          262 
          263         info.SetIcon(wx.Icon('icon.png', wx.BITMAP_TYPE_PNG))
          264         info.SetName('Cross Stitch')
          265         info.SetVersion('1.01')
          266         info.SetDescription(description)
          267         info.SetCopyright('(C) 2014 Anders Damsgaard')
          268         info.SetWebSite('https://github.com/anders-dc/cross-stitch')
          269         info.SetLicense(license)
          270         info.AddDeveloper('Anders Damsgaard')
          271         info.AddDocWriter('Anders Damsgaard')
          272         info.AddArtist('Anders Damsgaard')
          273 
          274         wx.AboutBox(info)
          275 
          276 
          277 
          278 def main():
          279     app = wx.App()
          280     MainScreen(None, title='Cross Stitch')
          281     app.MainLoop()
          282 
          283 if __name__ == '__main__':
          284     main()