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