class Frame(SimpleDoc, mongoengine.Document): """ Frame Objects are a mongo-friendly wrapper for SimpleCV image objects, containing additional properties for the originating camera and time of capture. Note that Frame.image property must be used as a getter-setter. >>> f = SimpleSeer.capture()[0] #get a frame from the SimpleSeer module >>> f.image.dl().line((0,0),(100,100)) >>> f.save() >>> """ capturetime = mongoengine.DateTimeField() camera = mongoengine.StringField() features = mongoengine.ListField(mongoengine.EmbeddedDocumentField(FrameFeature)) results = mongoengine.ListField(mongoengine.EmbeddedDocumentField(ResultEmbed)) #features height = mongoengine.IntField(default = 0) width = mongoengine.IntField(default = 0) imgfile = mongoengine.FileField() layerfile = mongoengine.FileField() thumbnail_file = mongoengine.FileField() _imgcache = '' meta = { 'indexes': ["capturetime", ('camera', '-capturetime')] } @LazyProperty def thumbnail(self): if self.thumbnail_file.grid_id is None: img = self.image thumbnail_img = img.scale(140.0 / img.height) img_data = StringIO() thumbnail_img.save(img_data, "jpeg", quality = 25) self.thumbnail_file.put(img_data.getvalue(), content_type='image/jpeg') else: self.thumbnail_file.get().seek(0,0) thumbnail_img = Image(pil.open(StringIO(self.thumbnail_file.read()))) return thumbnail_img @property def image(self): if self._imgcache != '': return self._imgcache self.imgfile.get().seek(0,0) #hackity hack, make sure the FP is at 0 if self.imgfile != None: try: self._imgcache = Image(pil.open(StringIO(self.imgfile.read()))) except (IOError, TypeError): # pragma no cover self._imgcache = None else: # pragma no cover self._imgcache = None if self.layerfile: self.layerfile.get().seek(0,0) self._imgcache.dl()._mSurface = pygame.image.fromstring(self.layerfile.read(), self._imgcache.size(), "RGBA") return self._imgcache @image.setter def image(self, value): self.width, self.height = value.size() self._imgcache = value def __repr__(self): # pragma no cover return "<SimpleSeer Frame Object %d,%d captured with '%s' at %s>" % ( self.width, self.height, self.camera, self.capturetime.ctime()) def save(self, *args, **kwargs): #TODO: sometimes we want a frame with no image data, basically at this #point we're trusting that if that were the case we won't call .image realtime.ChannelManager().publish('frame.', self) if self._imgcache != '': s = StringIO() img = self._imgcache img.getPIL().save(s, "jpeg", quality = 100) self.imgfile.delete() self.imgfile.put(s.getvalue(), content_type = "image/jpg") if len(img._mLayers): if len(img._mLayers) > 1: mergedlayer = DrawingLayer(img.size()) for layer in img._mLayers[::-1]: layer.renderToOtherLayer(mergedlayer) else: mergedlayer = img.dl() self.layerfile.delete() self.layerfile.put(pygame.image.tostring(mergedlayer._mSurface, "RGBA")) #TODO, make layerfile a compressed object #self._imgcache = '' super(Frame, self).save(*args, **kwargs) #TODO, this is sloppy -- we should handle this with cascading saves #or some other mechanism for r in self.results: result,created = Result.objects.get_or_create(auto_save=False, id=r.result_id) result.capturetime = self.capturetime result.camera = self.camera result.frame = self.id result.inspection = r.inspection_id result.measurement = r.measurement_id result.string = r.string result.numeric = r.numeric result.save(*args, **kwargs) def serialize(self): s = StringIO() try: self.image.save(s, "webp", quality = 80) return dict( content_type='image/webp', data=s.getvalue()) except KeyError: self.image.save(s, "jpeg", quality = 80) return dict( content_type='image/jpeg', data=s.getvalue()) @classmethod def search(cls, filters, sorts, skip, limit): db = cls._get_db() # Use the agg fmwk to generate frame ids pipeline = [ # initial match to reduce number of frames to search {'$match': filters }, # unwind features and results so we can do complex queries # against a *single* filter/result {'$unwind': '$features'}, {'$unwind': '$results' }, # Re-run the queries {'$match': filters }, {'$sort': sorts }, {'$project': {'_id': 1} }] cmd = db.command('aggregate', 'frame', pipeline=pipeline) total_frames = len(cmd['result']) seen = set() ids = [] # We have to do skip/limit in Python so we can skip over duplicate frames for doc in cmd['result']: id = doc['_id'] if id in seen: continue seen.add(id) if skip > 0: skip -= 1 continue ids.append(id) if len(ids) >= limit: break frames = cls.objects.filter(id__in=ids) frame_index = dict( (f.id, f) for f in frames) chosen_frames = [] for id in ids: frame = frame_index.get(id) if frame is None: continue chosen_frames.append(frame) return total_frames, chosen_frames
class Frame(SimpleDoc, mongoengine.Document): """ Frame Objects are a mongo-friendly wrapper for SimpleCV image objects, containing additional properties for the originating camera and time of capture. Note that Frame.image property must be used as a getter-setter. >>> f = SimpleSeer.capture()[0] #get a frame from the SimpleSeer module >>> f.image.dl().line((0,0),(100,100)) >>> f.save() >>> """ capturetime = mongoengine.DateTimeField() capturetime_epoch = mongoengine.IntField(default=0) updatetime = mongoengine.DateTimeField() camera = mongoengine.StringField() features = mongoengine.ListField(mongoengine.EmbeddedDocumentField(FrameFeature)) results = mongoengine.ListField(mongoengine.EmbeddedDocumentField(ResultEmbed)) height = mongoengine.IntField(default=0) width = mongoengine.IntField(default=0) clip_id = mongoengine.ObjectIdField(default=None) clip_frame = mongoengine.IntField(default=None) imgfile = mongoengine.FileField() layerfile = mongoengine.FileField() thumbnail_file = mongoengine.FileField() metadata = mongoengine.DictField() notes = mongoengine.StringField() _imgcache = "" _imgcache_dirty = False meta = { "indexes": [ "capturetime", "-capturetime", ("camera", "-capturetime"), "-capturetime_epoch", "capturetime_epoch", "results", "results.state", "metadata", ] } @classmethod # which fields we care about for Filter.py def filterFieldNames(cls): return [ "_id", "camera", "capturetime", "capturetime_epoch", "metadata", "notes", "height", "width", "imgfile", "features", "results", ] @LazyProperty def thumbnail(self): if self.thumbnail_file is None or self.thumbnail_file.grid_id is None: img = self.image thumbnail_img = img.scale(140.0 / img.height) if self.id and not "is_slave" in Session().mongo: img_data = StringIO() thumbnail_img.save(img_data, "jpeg", quality=75) self.thumbnail_file.put(img_data.getvalue(), content_type="image/jpeg") self.save() else: self.thumbnail_file.get().seek(0, 0) thumbnail_img = Image(pil.open(StringIO(self.thumbnail_file.read()))) return thumbnail_img @LazyProperty def clip(self): return Clip.objects.get(id=self.clip_id) @property def image(self): if self._imgcache != "": return self._imgcache if self.clip_id is not None: return self.clip.images[self.clip_frame] self.imgfile.get().seek(0, 0) # hackity hack, make sure the FP is at 0 if self.imgfile and self.imgfile.grid_id != None: try: self._imgcache = Image(pil.open(StringIO(self.imgfile.read()))) except (IOError, TypeError): # pragma no cover self._imgcache = None else: # pragma no cover self._imgcache = None if self.layerfile: self.layerfile.get().seek(0, 0) self._imgcache.dl()._mSurface = pygame.image.fromstring( self.layerfile.read(), self._imgcache.size(), "RGBA" ) return self._imgcache @image.setter def image(self, value): self._imgcache_dirty = True self.width, self.height = value.size() self._imgcache = value def save_image(self): if self._imgcache != "" and self._imgcache_dirty: s = StringIO() img = self._imgcache if self.clip_id is None: img.getPIL().save(s, "jpeg", quality=100) self.imgfile.replace(s.getvalue(), content_type="image/jpg") if len(img._mLayers): if len(img._mLayers) > 1: mergedlayer = DrawingLayer(img.size()) for layer in img._mLayers[::-1]: layer.renderToOtherLayer(mergedlayer) else: mergedlayer = img.dl() self.layerfile.replace(pygame.image.tostring(mergedlayer._mSurface, "RGBA")) # TODO, make layerfile a compressed object # self._imgcache = '' self._imgcache_dirty = False return self.imgfile.grid_id def delete_image(self): self.imgfile.delete() self._imgcache = "" self._imgcache_dirty = False def has_image_data(self): if self.clip_id and self.clip: return True if self.imgfile and self.imgfile.grid_id != None: return True return False def __repr__(self): # pragma no cover capturetime = "???" if self.capturetime: capturetime = self.capturetime.ctime() return "<SimpleSeer Frame Object %d,%d captured with '%s' at %s>" % ( self.width, self.height, self.camera, capturetime, ) def save(self, *args, **kwargs): # TODO: sometimes we want a frame with no image data, basically at this # point we're trusting that if that were the case we won't call .image self.save_image() epoch_ms = timegm(self.capturetime.timetuple()) * 1000 + self.capturetime.microsecond / 1000 if self.capturetime_epoch != epoch_ms: self.capturetime_epoch = epoch_ms # Aggregate the tolerance states into single measure self.metadata["tolstate"] = "Pass" for r in self.results: if r.state > 0: self.metadata["tolstate"] = "Fail" self.updatetime = datetime.utcnow() super(Frame, self).save(*args, **kwargs) # TODO, this is sloppy -- we should handle this with cascading saves # or some other mechanism # for r in self.results: # result, created = r.get_or_create_result() # result.capturetime = self.capturetime # result.frame_id = self.id # result.save(*args, **kwargs) # Once everything else is saved, publish result # Do not place any other save actions after this line or realtime objects will miss data realtime.ChannelManager().publish("frame/", self) def serialize(self): s = StringIO() try: self.image.save(s, "webp", quality=80) return dict(content_type="image/webp", data=s.getvalue()) except KeyError: self.image.save(s, "jpeg", quality=80) return dict(content_type="image/jpeg", data=s.getvalue()) @classmethod def search(cls, filters, sorts, skip, limit): db = cls._get_db() # Use the agg fmwk to generate frame ids pipeline = [ # initial match to reduce number of frames to search {"$match": filters}, # unwind features and results so we can do complex queries # against a *single* filter/result {"$unwind": "$features"}, {"$unwind": "$results"}, # Re-run the queries {"$match": filters}, {"$sort": sorts}, {"$project": {"_id": 1}}, ] cmd = db.command("aggregate", "frame", pipeline=pipeline) total_frames = len(cmd["result"]) seen = set() ids = [] # We have to do skip/limit in Python so we can skip over duplicate frames for doc in cmd["result"]: id = doc["_id"] if id in seen: continue seen.add(id) if skip > 0: skip -= 1 continue ids.append(id) if len(ids) >= limit: break frames = cls.objects.filter(id__in=ids) frame_index = dict((f.id, f) for f in frames) chosen_frames = [] for id in ids: frame = frame_index.get(id) if frame is None: continue chosen_frames.append(frame) earliest_date = None if "capturetime" not in filters: earliest_frame = cls.objects.filter(**filters).order_by("capturetime").limit(1).first() if earliest_frame: earliest_date = earliest_frame.capturetime return total_frames, chosen_frames, earliest_date