def compute_features_collection(snaps, pic_dir, cache=None): """Compute the features for every element in the paths""" if cache is None: cache = {} snaps_with_features = [] features_list = [] for snap in snaps: snap_id = snap.filename features = cache.get(snap_id) if features is not None: snaps_with_features.append(snap) features_list.append(features) continue # Compute the features if not snap.get('warped', False): logger.warning('Cannot compute feature for unwarped %s', snap_id) continue warped_file_path = os.path.join(pic_dir, snap.warped_filename) new_features = compute_features(warped_file_path) cache[snap_id] = new_features snaps_with_features.append(snap) features_list.append(new_features) return snaps_with_features, np.array(features_list)
def snapshot(request): """Index page.""" filename = request.matchdict['file'] settings = dict(request.registry.settings) pic_dir = settings['thumbs.document_root'] file_path = os.path.join(pic_dir, filename) try: height, width = get_img_size(file_path) except IOError: logger.error("Failed to open file with path: %s", file_path, exc_info=True) raise HTTPNotFound("Could not load image for snap %s" % filename) # security loop elmts = filename.split(os.sep) for unsecure in ('.', '..'): if unsecure in elmts: return HTTPNotFound() file_uuid = os.path.splitext(filename)[0] if request.method == 'POST': base = _toint(request.POST['bottom_y']), _toint(request.POST['bottom_x']) top = _toint(request.POST['top_y']), _toint(request.POST['top_x']) warped_img_path, new_base, new_top = warp_img(file_path, base, top) # There should be only one snap_idx, snap_type = 'snaps', 'snaptype' snap = request.db.get(snap_idx, snap_type, file_uuid) warped_filename = os.path.basename(warped_img_path) if snap is not None: snap['warped_filename'] = warped_filename snap['warped'] = True snap['base_x'] = base[0] snap['base_y'] = base[1] snap['top_x'] = top[0] snap['top_y'] = top[1] logger.debug("Warping snap %s", file_uuid) request.db.index(snap, snap_idx, snap_type, file_uuid) request.db.refresh() # Precompute the cached features incrementally compute_features_collection([snap], pic_dir, cache=FEATURES_CACHE) else: logger.warning("Could not find snap for %s", file_uuid) return HTTPFound(location='/warped/%s' % warped_filename) data = {'snapshot': filename, 'width': width, 'height': height} return _basic(request, data)
def warped(request): """Index page.""" filename = request.matchdict['file'] settings = dict(request.registry.settings) pic_dir = settings['thumbs.document_root'] orig_img = os.path.join(pic_dir, filename) res = get_img_size(orig_img) warped_stemname = os.path.splitext(filename)[0] snap_idx, snap_type = 'snaps', 'snaptype' if warped_stemname.endswith('_warped'): file_uuid = warped_stemname[:-len('_warped')] else: logger.warning( 'Warped stemname is expected to end with "_warped", got: %s', warped_stemname ) file_uuid = warped_stemname try: current_snap = request.db.get(snap_idx, snap_type, file_uuid) except NotFoundException: raise HTTPNotFound("Could not find snapshot for %s" % file_uuid) next_snap = None unwarped_count = "" if not current_snap.plant: query = FieldQuery(FieldParameter('warped', True)) candidates = request.db.search(query, size=200, indices=['snaps'], sort='timestamp:desc') # TODO: filter out non-plant snaps in the query directly candidates = [c for c in candidates if c.plant != None] logger.debug('Computing suggestion for %s', current_snap.filename) best_snaps, scores = suggest_snaps(current_snap, candidates, pic_dir, FEATURES_CACHE) suggestions = OrderedDict() for i, (snap, score) in enumerate(zip(best_snaps, scores)): name = snap.plant logger.debug("#%02d: %s with score = %f - %s", i, name, score, snap.filename) suggestion = suggestions.get(name) if suggestion is None: query = FieldQuery(FieldParameter('name', name)) plants = list(request.db.search(query, indices=['plants'])) if not plants: raise HTTPNotFound("No plant registered under %s" % name) plant = plants[0] suggestions[name] = (plant, [snap]) else: suggestion[1].append(snap) else: suggestions = None # Suggest to warp the next unwarped snapshot from the same plant query = FieldQuery(fieldparameters=( FieldParameter('warped', None), FieldParameter('filename', '-' + current_snap.filename), FieldParameter('plant', current_snap.plant))) max_count = 100 next_snaps = request.db.search({ "bool" : { "must" : [ {'field': {'filename': '-' + current_snap.filename}}, {'field': {'plant': current_snap.plant}}, ], "must_not" : [ {'field': {'warped': True}}, ], }, }, size=max_count, indices=['snaps'], sort='timestamp:desc') logger.debug(len(next_snaps)) if len(next_snaps) > 0: next_snap = next_snaps[0] unwarped_count = "%d" % len(next_snaps) if len(next_snaps) == max_count: unwarped_count += "+" if len(res) == 2: height, width = res else: height, width = 500, 500 # security loop elmts = filename.split(os.sep) for unsecure in ('.', '..'): if unsecure in elmts: return HTTPNotFound() data = {'snapshot': filename, 'original': get_original_path(filename), 'width': width, 'height': height, 'snap': current_snap, 'uuid': file_uuid, 'suggestions': suggestions, 'next_snap': next_snap, 'unwarped_count': unwarped_count} return _basic(request, data)
def warp_img(raw_img_path, base, top, warped_img_size=WARPED_IMG_SIZE): """Rotate and rescale to normalize the position of base and top points Return the file system path of the transformed image and the transformed base and top positions in the new image. """ # Load the data original = imread(raw_img_path) base = np.array(base) top = np.array(top) # Check the expected size of the resulting file warped_img_size = np.array(warped_img_size) warped_h, warped_w = warped_img_size bigger = np.max(original.shape[:2]) * 1.5 embedded = np.zeros(shape=(bigger, bigger, original.shape[2]), dtype=original.dtype) original_center = (base + top) / 2 embedded_center = np.array(embedded.shape[:2]) / 2 offset = embedded_center - original_center x_slice = slice(offset[0], offset[0] + original.shape[0]) y_slice = slice(offset[1], offset[1] + original.shape[1]) try: embedded[x_slice, y_slice, :] = original except ValueError: logger.warning("Invalid base and top positions on image %s: %r, %r", raw_img_path, base, top) return raw_img_path, tuple(base), tuple(top) # Check the original base and top coordinate positions embedded_base = base + offset embedded_top = top + offset warped_base = ((0.9, 0.5) * warped_img_size).astype(np.int) warped_top = ((0.05, 0.5) * warped_img_size).astype(np.int) logger.debug("About to warp from {base, top = %r, %r} to " "{warped_base, warped_top = %r, %r}", base, top, warped_base, warped_top) orig_vector = embedded_top - embedded_base orig_norm = np.linalg.norm(orig_vector) warped_vector = warped_top - warped_base warped_norm = np.linalg.norm(warped_vector) if orig_norm == 0: logger.warning("Cannot warp image using a null original vector") # Cannot warp with this data, return null operation insted return raw_img_path, tuple(base), tuple(top) scale = warped_norm / orig_norm rotation = (np.arctan2(warped_vector[0], warped_vector[1]) - np.arctan2(orig_vector[0], orig_vector[1])) rotation_degrees = rotation * -180. / np.pi pil_rotated = Image.fromarray(embedded).rotate(rotation_degrees) scaled_size = (int(embedded.shape[0] * scale), int(embedded.shape[1] * scale)) logger.debug("Warping from %r to %r using rot=%0.2f scale=%0.2f", orig_vector, warped_vector, rotation_degrees, scale) pil_scaled = pil_rotated.resize(scaled_size, Image.ANTIALIAS) scaled = np.array(pil_scaled) scaled_center = np.array(scaled.shape[:2]) / 2 x_slice = slice(scaled_center[0] - warped_h / 2, scaled_center[0] + warped_h / 2) y_slice = slice(scaled_center[1] - warped_w / 2, scaled_center[1] + warped_w / 2) warped = scaled[x_slice, y_slice, :] # Save the result back on the harddrive warped_path = get_warped_img_path(raw_img_path) imsave(warped_path, warped) return warped_path, tuple(warped_base), tuple(warped_top)