Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from builtins import str 

2# cumulative one dimensional movie slicer 

3import os 

4import warnings 

5import subprocess 

6from subprocess import CalledProcessError 

7import numpy as np 

8from functools import wraps 

9 

10from lsst.sims.maf.utils import percentileClipping, optimalBins 

11from lsst.sims.maf.stackers import ColInfo 

12from .baseSlicer import BaseSlicer 

13 

14__all__ = ['MovieSlicer'] 

15 

16class MovieSlicer(BaseSlicer): 

17 def __init__(self, sliceColName=None, sliceColUnits=None, 

18 bins=None, binMin=None, binMax=None, binsize=None, 

19 verbose=True, badval=0, cumulative=True, forceNoFfmpeg=False): 

20 """ 

21 The 'movieSlicer' acts similarly to the OneDSlicer (slices on one data column). 

22 However, the data slices from the movieSlicer are intended to be fed to another slicer, which then 

23 (together with a set of Metrics) calculates metric values + plots at each slice created by the movieSlicer. 

24 The job of the movieSlicer is to track those slices and put them together into a movie. 

25 

26 'sliceColName' is the name of the data column to use for slicing. 

27 'sliceColUnits' lets the user set the units (for plotting purposes) of the slice column. 

28 'bins' can be a numpy array with the binpoints for sliceCol or a single integer value 

29 (if a single value, this will be used as the number of bins, together with data min/max or binMin/Max), 

30 as in numpy's histogram function. 

31 If 'binsize' is used, this will override the bins value and will be used together with the data min/max 

32 or binMin/Max to set the binpoint values. 

33 

34 Bins work like numpy histogram bins: the last 'bin' value is end value of last bin; 

35 all bins except for last bin are half-open ([a, b>), the last one is ([a, b]). 

36 

37 The movieSlicer stitches individual frames together into a movie using ffmpeg. Thus, on 

38 instantiation it checks that ffmpeg is available and will raise and exception if not. 

39 This behavior can be overriden using forceNoFfmpeg = True (in order to create a movie later perhaps). 

40 """ 

41 # Check for ffmpeg. 

42 if not forceNoFfmpeg: 

43 try: 

44 fnull = open(os.devnull, 'w') 

45 subprocess.check_call(['which', 'ffmpeg'], stdout=fnull) 

46 except CalledProcessError: 

47 raise Exception('Could not find ffmpeg on the system, so will not be able to create movie.' 

48 ' Use forceNoFfmpeg=True to override this error and create individual images.') 

49 super().__init__(verbose=verbose, badval=badval) 

50 self.sliceColName = sliceColName 

51 self.columnsNeeded = [sliceColName] 

52 self.bins = bins 

53 self.binMin = binMin 

54 self.binMax = binMax 

55 self.binsize = binsize 

56 self.cumulative = cumulative 

57 if sliceColUnits is None: 

58 co = ColInfo() 

59 self.sliceColUnits = co.getUnits(self.sliceColName) 

60 self.slicer_init = {'sliceColName':self.sliceColName, 'sliceColUnits':sliceColUnits, 

61 'badval':badval} 

62 

63 def setupSlicer(self, simData, maps=None): 

64 """ 

65 Set up bins in slicer. 

66 """ 

67 if self.sliceColName is None: 

68 raise Exception('sliceColName was not defined when slicer instantiated.') 

69 sliceCol = simData[self.sliceColName] 

70 # Set bin min/max values. 

71 if self.binMin is None: 

72 self.binMin = sliceCol.min() 

73 if self.binMax is None: 

74 self.binMax = sliceCol.max() 

75 # Give warning if binMin = binMax, and do something at least slightly reasonable. 

76 if self.binMin == self.binMax: 

77 warnings.warn('binMin = binMax (maybe your data is single-valued?). ' 

78 'Increasing binMax by 1 (or 2*binsize, if binsize set).') 

79 if self.binsize is not None: 

80 self.binMax = self.binMax + 2 * self.binsize 

81 else: 

82 self.binMax = self.binMax + 1 

83 # Set bins. 

84 # Using binsize. 

85 if self.binsize is not None: 

86 if self.bins is not None: 

87 warnings.warn('Both binsize and bins have been set; Using binsize %f only.' %(self.binsize)) 

88 self.bins = np.arange(self.binMin, self.binMax+self.binsize/2.0, float(self.binsize), 'float') 

89 # Using bins value. 

90 else: 

91 # Bins was a sequence (np array or list) 

92 if hasattr(self.bins, '__iter__'): 

93 self.bins = np.sort(self.bins) 

94 # Or bins was a single value. 

95 else: 

96 if self.bins is None: 

97 self.bins = optimalBins(sliceCol, self.binMin, self.binMax) 

98 nbins = int(self.bins) 

99 self.binsize = (self.binMax - self.binMin) / float(nbins) 

100 self.bins = np.arange(self.binMin, self.binMax+self.binsize/2.0, self.binsize, 'float') 

101 # Set nbins to be one less than # of bins because last binvalue is RH edge only 

102 self.nslice = len(self.bins) - 1 

103 # Set slicePoint metadata. 

104 self.slicePoints['sid'] = np.arange(self.nslice) 

105 self.slicePoints['bins'] = self.bins 

106 # Add metadata from maps. 

107 self._runMaps(maps) 

108 # Set up data slicing. 

109 self.simIdxs = np.argsort(simData[self.sliceColName]) 

110 simFieldsSorted = np.sort(simData[self.sliceColName]) 

111 # "left" values are location where simdata == bin value 

112 self.left = np.searchsorted(simFieldsSorted, self.bins[:-1], 'left') 

113 self.left = np.concatenate((self.left, np.array([len(self.simIdxs),]))) 

114 # Set up _sliceSimData method for this class. 

115 if self.cumulative: 

116 @wraps(self._sliceSimData) 

117 def _sliceSimData(islice): 

118 """ 

119 Slice simData on oneD sliceCol, to return relevant indexes for slicepoint. 

120 """ 

121 #this is the important part. The ids here define the pieces of data that get 

122 #passed on to subsequent slicers 

123 #cumulative version of 1D slicing 

124 idxs = self.simIdxs[0:self.left[islice+1]] 

125 return {'idxs':idxs, 

126 'slicePoint':{'sid':islice, 'binLeft':self.bins[0], 'binRight':self.bins[islice+1]}} 

127 setattr(self, '_sliceSimData', _sliceSimData) 

128 else: 

129 @wraps(self._sliceSimData) 

130 def _sliceSimData(islice): 

131 """ 

132 Slice simData on oneD sliceCol, to return relevant indexes for slicepoint. 

133 """ 

134 idxs = self.simIdxs[self.left[islice]:self.left[islice+1]] 

135 return {'idxs':idxs, 

136 'slicePoint':{'sid':islice, 'binLeft':self.bins[islice], 'binRight':self.bins[islice+1]}} 

137 setattr(self, '_sliceSimData', _sliceSimData) 

138 

139 def __eq__(self, otherSlicer): 

140 """ 

141 Evaluate if slicers are equivalent. 

142 """ 

143 if isinstance(otherSlicer, MovieSlicer): 

144 return np.all(otherSlicer.slicePoints['bins'] == self.slicePoints['bins']) 

145 else: 

146 return False 

147 

148 def makeMovie(self, outfileroot, sliceformat, plotType, figformat, outDir='Output', ips=10.0, fps=10.0): 

149 """ 

150 Takes in metric and slicer metadata and calls ffmpeg to stitch together output files. 

151 """ 

152 if not os.path.isdir(outDir): 

153 raise Exception('Cannot find output directory %s with movie input files.' %(outDir)) 

154 #make video 

155 # ffmpeg -r 30 -i [image names - FilterColors_SkyMap_%03d.png] -r 30 

156 # -pix_fmt yuv420p -crf 18 -preset slower outfile 

157 callList = ['ffmpeg', '-r', str(ips), '-i', 

158 os.path.join(outDir,'%s_%s_%s.%s'%(outfileroot, sliceformat, plotType, figformat)), 

159 '-r', str(fps), '-pix_fmt', 'yuv420p', '-crf', '18', '-preset', 'slower', 

160 os.path.join(outDir,'%s_%s_%s_%s.mp4' %(outfileroot, plotType, str(ips), str(fps)))] 

161 print('Attempting to call ffmpeg with:') 

162 print(' '.join(callList)) 

163 p = subprocess.check_call(callList) 

164 #make thumbnail gif 

165 callList = ['ffmpeg','-i',os.path.join(outDir,'%s_%s_%s_%s.mp4' %(outfileroot, plotType, str(ips), str(fps))), 

166 '-vf', 'scale=%s:%s' %(str(320),str(-1)), '-t', str(10), '-r', str(10), 

167 os.path.join(outDir,'%s_%s_%s_%s.gif' %(outfileroot, plotType, str(ips), str(fps)))] 

168 print('converting to animated gif with:') 

169 print(' '.join(callList)) 

170 p2 = subprocess.check_call(callList)