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