def serve_mosaic_image(time_series=None, pid=None, params='/'): """Generate a mosaic of ROIs from a sample bin. params include the following, with default values - series (mvco) - time series (FIXME: handle with resolver, clarify difference between namespace, time series, pid, lid) - size (1024x1024) - size of the mosaic image - page (1) - page. for typical image sizes the entire bin does not fit and so is split into pages. - scale - scaling factor for image dimensions """ # parse params params = parse_params(params, size='1024x1024',page=1,scale=1.0) (w,h) = tuple(map(int,re.split('x',params[SIZE]))) scale = float(params[SCALE]) page = int(params[PAGE]) # parse pid/lid hit = pid_resolver.resolve(pid=pid) # perform layout operation scaled_size = (int(w/scale), int(h/scale)) layout = get_mosaic_layout(hit.bin_pid, scaled_size, page) # serve JSON on request if hit.extension == 'json': return jsonr(list(layout2json(layout, scale))) # resolve ROI file roi_path = resolve_roi(hit.bin_pid) # read all images needed for compositing and inject into Tiles with open(roi_path,'rb') as roi_file: for tile in layout: target = tile.image # FIXME use fast stitching tile.image = as_pil(get_fast_stitched_roi(hit.bin_pid, target[TARGET_NUMBER])) # produce and serve composite image mosaic_image = thumbnail(mosaic.composite(layout, scaled_size, mode='L', bgcolor=160), (w,h)) (pil_format, mimetype) = image_types(hit) return image_response(mosaic_image, pil_format, mimetype)
def im2bytes(im): """Convert a numpy uint8 array to a bytearray""" im = as_pil(im) buf = BytesIO() with tempfile.SpooledTemporaryFile() as imtemp: im.save(imtemp,'PNG') imtemp.seek(0) shutil.copyfileobj(imtemp, buf) return buf.getvalue()
def stitch_raw(targets,images,box=None,background=0): # compute bounds relative to the camera field if box is None: box = stitched_box(targets) (x,y,w,h) = box # now we swap width and height to rotate the image 90 degrees s = Image.new('L',(h,w),background) # the stitched image with a missing region for (roi,image) in zip(targets,images): image = as_pil(image) rx = roi[LEFT] - x ry = roi[BOTTOM] - y rw = roi[WIDTH] rh = roi[HEIGHT] roi_box = (ry, rx, ry + rh, rx + rw) s.paste(image, roi_box) # paste in the right location return np.array(s)
def image_response(image,format,mimetype): """Construct a Flask Response object for the given image, PIL format, and MIME type.""" buf = StringIO() im = as_pil(image).save(buf,format) return Response(buf.getvalue(), mimetype=mimetype)
def bright_mv(image,mask=None): mask = as_pil(mask) eh = image.histogram(mask) # toast extrema return bright_mv_hist(eh)
def stitch(targets,images): # compute bounds relative to the camera field (x,y,w,h) = stitched_box(targets) # note that w and h are switched from here on out to rotate 90 degrees. # step 1: compute masks s = as_pil(stitch_raw(targets,images,(x,y,w,h))) # stitched ROI's with black gaps rois_mask = as_pil(mask(targets)) # a mask of where the ROI's are gaps_mask = ImageChops.invert(rois_mask) # its inverse is where the gaps are edges = edges_mask(targets,images) # edges are pixels along the ROI edges # step 2: estimate background from edges # compute the mean and variance of the edges (mean,variance) = bright_mv(s,edges) # now use that as an estimated background flat_bg = Image.new('L',(h,w),mean) # FIXME s.paste(mean,None,gaps_mask) # step 3: compute "probable background": low luminance delta from estimated bg bg = extract_background(s,flat_bg) # also mask out the gaps, which are not "probable background" bg.paste(255,None,gaps_mask) # step 3a: improve mean/variance estimate (mean,variance) = bright_mv(bg) std_dev = sqrt(variance) # step 4: sample probable background to compute RBF for illumination gradient # grid div = 6 means = [] nodes = [] rad = avg([h,w]) / div rad_step = int(rad/2)+1 for x in range(0,h+rad,rad): for y in range(0,w+rad,rad): for r in range(rad,max(h,w),int(rad/3)+1): box = (max(0,x-r),max(0,y-r),min(h-1,x+r),min(w-1,y+r)) region = bg.crop(box) nabe = region.histogram() (m,v) = bright_mv_hist(nabe) if m > 0 and m < 255: # reject outliers nodes.append((x,y)) means.append(m) break # now construct radial basis functions for mean, based on the samples mean_rbf = interpolate.Rbf([x for x,y in nodes], [y for x,y in nodes], means, epsilon=rad) # step 5: fill gaps with mean based on RBF and variance from bright_mv(edges) mask_pix = gaps_mask.load() noise = Image.new('L',(h,w),mean) noise_pix = noise.load() np.random.seed(0) gaussian = np.random.normal(0, 1.0, size=(h,w)) # it's normal std_dev *= 0.66 # err on the side of smoother rather than noisier mask_x = [] mask_y = [] for x in xrange(h): for y in xrange(w): if mask_pix[x,y] == 255: # only for pixels in the mask mask_x.append(x) mask_y.append(y) rbf_fill = mean_rbf(np.array(mask_x), np.array(mask_y)) for x,y,r in zip(mask_x, mask_y, rbf_fill): # fill is illumination gradient + noise noise_pix[x,y] = r + (gaussian[x,y] * std_dev) # step 6: final composite s.paste(noise,None,gaps_mask) return (np.array(s),rois_mask)