cross-stitch

interactively turn images into patterns for cross stitching
git clone git://src.adamsgaard.dk/cross-stitch # fast
git clone https://src.adamsgaard.dk/cross-stitch.git # slow
Log | Files | Refs | README | LICENSE Back to index

cross-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()