Coverage for python/lsst/summit/extras/imageSorter.py: 13%

128 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-20 01:22 -0800

1# This file is part of summit_extras. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import pickle 

23from PIL import Image 

24import matplotlib.pyplot as plt 

25import os 

26from os import system 

27import re 

28 

29 

30TAGS = """ 

31 - (Blank/no annotation) - nominally good, i.e. nothing notable in the image 

32Q - bad main star location (denoted by cross-hair on image sorter) 

33F - Obviously very poor focus (worse than just seeing, does NOT include donuts) 

34D - Donut image 

35O - Occlusion (dome or door) 

36V - No back bias suspected 

37P - Non-standard PSF (rotator/mount issues/tracking error, etc) 

38S - Satellite or plane crossing image 

39! - Something interesting/crazy - see notes on image 

40""" 

41 

42INSTRUCTIONS = (TAGS + '\n' + 

43 """ 

44 = - apply the same annotations as the previous image 

45 To enter no tags but some notes, just start with a space 

46 """) 

47 

48 

49class ImageSorter(): 

50 """Take a list on png files, as created by lsst.summit.extras.animator 

51 and tag each dataId with a number of attributes. 

52 

53 Returns a dict of dataId dictionaries with values being the corresponding 

54 """ 

55 

56 def __init__(self, fileList, outputFilename): 

57 self.fileList = fileList 

58 self.outputFilename = outputFilename 

59 

60 @staticmethod 

61 def _getDataIdFromFilename(filename): 

62 # filename of the form 2021-02-18-705-quickLookExp.png 

63 filename = os.path.basename(filename) 

64 mat = re.match(r'^(\d{4}-\d{2}-\d{2})-(\d*)-.*$', filename) 

65 if not mat: 

66 raise RuntimeError(f"Failed to extract dayObs/seqNum from {filename}") 

67 dayObs = mat.group(1) 

68 seqNum = int(mat.group(2)) 

69 return (dayObs, seqNum) 

70 

71 def getPreviousAnnotation(self, info, imNum): 

72 if imNum == 0: 

73 raise RuntimeError("There is no previous annotation for the first image.") 

74 

75 previousFilename = self.fileList[imNum-1] 

76 previousDataId = self._getDataIdFromFilename(previousFilename) 

77 previousAnnotation = info[previousDataId] 

78 return previousAnnotation 

79 

80 def addData(self, dataId, info, answer, mode, imNum): 

81 """Modes = O(verwrite), S(kip), A(ppend)""" 

82 if '=' in answer: 

83 answer = self.getPreviousAnnotation(info, imNum) 

84 

85 if dataId not in info: 

86 info[dataId] = answer 

87 return 

88 

89 if mode == 'O': 

90 info[dataId] = answer 

91 elif mode in ['B', 'A']: 

92 oldAnswer = info[dataId] 

93 answer = "".join([oldAnswer, answer]) 

94 info[dataId] = answer 

95 else: 

96 raise RuntimeError(f"Unrecognised mode {mode} - should be impossible") 

97 return 

98 

99 @classmethod 

100 def loadAnnotations(cls, pickleFilename): 

101 """Load back and split up annotations for easy use. 

102 

103 Anything after a space is returned as a whole string, 

104 anything before it is lower-cased and returned as tags. 

105 

106 from lsst.summit.extras import ImageSorter 

107 tags, notes = ImageSorter.loadAnnotations(pickleFilename) 

108 """ 

109 loaded = cls._load(pickleFilename) 

110 

111 tags, notes = {}, {} 

112 

113 for dataId, answerFull in loaded.items(): 

114 answer = answerFull.lower() 

115 if answerFull.startswith(' '): # notes only case 

116 tags[dataId] = '' 

117 notes[dataId] = answerFull.strip() 

118 continue 

119 

120 if " " in answer: 

121 answer = answerFull.split()[0] 

122 notes[dataId] = " ".join([_ for _ in answerFull.split()[1:]]) 

123 tags[dataId] = answer.upper() 

124 

125 return tags, notes 

126 

127 @staticmethod 

128 def _load(filename): 

129 """Internal loading only. 

130 

131 Not to be used by users for reading back annotations""" 

132 with open(filename, "rb") as pickleFile: 

133 info = pickle.load(pickleFile) 

134 return info 

135 

136 @staticmethod 

137 def _save(info, filename): 

138 with open(filename, "wb") as dumpFile: 

139 pickle.dump(info, dumpFile) 

140 

141 def sortImages(self): 

142 mode = 'A' 

143 info = {} 

144 if os.path.exists(self.outputFilename): 

145 info = self._load(self.outputFilename) 

146 

147 print(f'Output file {self.outputFilename} exists with info on {len(info)} files:') 

148 print('Press A - view all images, appending info to existing entries') 

149 print('Press O - view all images, overwriting existing entries') 

150 print('Press S - skip all images with existing annotations, including blank annotations') 

151 print('Press B - skip all images with annotations that are not blank') 

152 print('Press D - just display existing data and exit') 

153 print('Press Q to quit') 

154 mode = input() 

155 mode = mode[0].upper() 

156 

157 if mode == 'Q': 

158 exit() 

159 elif mode == 'D': 

160 for dataId, value in info.items(): 

161 print(f"{dataId[0]} - {dataId[1]}: {value}") 

162 exit() 

163 elif mode in 'AOSB': 

164 pass 

165 else: 

166 print("Unrecognised response - try again") 

167 self.sortImages() 

168 return # don't run twice in this case! 

169 

170 # need to write file first, even if empty, because _load and _save 

171 # are inside the loop to ensure that annotations aren't lost even on 

172 # full crash 

173 print(INSTRUCTIONS) 

174 self._save(info, self.outputFilename) 

175 

176 plt.figure(figsize=(10, 10)) 

177 for imNum, filename in enumerate(self.fileList): 

178 info = self._load(self.outputFilename) 

179 

180 dataId = self._getDataIdFromFilename(filename) 

181 if dataId in info and mode in ['S', 'B']: # always skip if found for S and if not blank for B 

182 if (mode == 'S') or (mode == 'B' and info[dataId] != ""): 

183 continue 

184 

185 with Image.open(filename) as pilImage: 

186 pilImage = Image.open(filename) 

187 width, height = pilImage.size 

188 cropLR, cropUD = 100-50, 180-50 

189 cropped = pilImage.crop((cropLR, cropUD, width-cropLR, height-cropUD)) 

190 plt.clf() 

191 plt.imshow(cropped, interpolation="bicubic") 

192 plt.show(block=False) 

193 plt.draw() # without this you get the same image each time 

194 plt.tight_layout() 

195 osascriptCall = '''/usr/bin/osascript -e 'tell app "Finder" to ''' 

196 osascriptCall += '''set frontmost of process "Terminal" to true' ''' 

197 system(osascriptCall) 

198 

199 oldAnswer = None # just so we can display existing info with the dataId 

200 if dataId in info: 

201 oldAnswer = info[dataId] 

202 inputStr = f"{dataId[0]} - {dataId[1]}: %s" % ("" if oldAnswer is None else oldAnswer) 

203 answer = input(inputStr) 

204 if 'exit' in answer: 

205 break # break don't exit so data is written! 

206 

207 self.addData(dataId, info, answer, mode, imNum) 

208 self._save(info, self.outputFilename) 

209 

210 print(f'Info written to {self.outputFilename}') 

211 

212 return info 

213 

214 

215if __name__ == '__main__': 215 ↛ 217line 215 didn't jump to line 217, because the condition on line 215 was never true

216 # TODO: DM-34239 Remove this 

217 fileList = ['/Users/merlin/rsync/animatorOutput/pngs/dayObs-2020-02-17-seqNum-232-calexp.png', 

218 '/Users/merlin/rsync/animatorOutput/pngs/dayObs-2020-02-17-seqNum-233-calexp.png', 

219 '/Users/merlin/rsync/animatorOutput/pngs/dayObs-2020-02-17-seqNum-234-calexp.png', 

220 '/Users/merlin/rsync/animatorOutput/pngs/dayObs-2020-02-17-seqNum-235-calexp.png'] 

221 

222 sorter = ImageSorter(fileList, '/Users/merlin/scratchfile.txt') 

223 sorter.sortImages()