def do_tubefitting(im_path=im_test_path,
                   metadata_path=metadata_test_path,
                   output_path=output_path,
                   save_output=False):
    # todo: fix things so that all operations use a consistent definition of background rather than changing Prefs on the fly...
    Prefs.blackBackground = False
    info = PrescreenInfo()
    info.load_info_from_json(metadata_path)
    z_xy_ratio = abs(
        info.get_z_plane_spacing_um()) / info.get_xy_pixel_size_um()
    #z_xy_ratio = 1.0;
    bfimp = bf.openImagePlus(im_path)
    imp = bfimp[0]
    imp.show()
    IJ.run(imp, "Set Scale...", "distance=0 known=0 pixel=1 unit=pixel")
    imp = utils.downsample_for_isotropy(imp,
                                        extra_downsample_factor=1.0,
                                        info=info)
    rot_seg_imp, rot_proj_imp, egfp_mch_imps = split_and_rotate(imp, info)
    depth = rot_seg_imp.getNSlices() if rot_seg_imp.getNSlices(
    ) > rot_seg_imp.getNFrames() else rot_seg_imp.getNFrames()
    width = rot_seg_imp.getWidth()
    height = int(round(rot_seg_imp.getHeight() * z_xy_ratio))

    # Apply 3d MEDIAN FILTER to denoise and emphasise vessel-associated voxels
    fit_basis_imp = threshold_and_binarise(rot_seg_imp, z_xy_ratio)
    fit_basis_imp.setTitle("fit_basis_imp")
    fit_basis_imp.show()

    # plane-wise, use binary-outline
    # say the non-zero points then make up basis for fitting to be performed per http://nicky.vanforeest.com/misc/fitEllipse/fitEllipse.html
    rois = []
    centres = []
    major_axes = []
    roi_imp = IJ.createImage("rois", width, height, depth, 32)
    pts_stack = ImageStack(width, height + 1)
    IJ.run(imp, "Line Width...", "line=3")
    for zidx in range(fit_basis_imp.getNSlices()):
        fit_basis_imp.setZ(zidx + 1)
        IJ.run(fit_basis_imp, "Outline", "slice")
        IJ.run(fit_basis_imp, "Create Selection", "")
        roi = fit_basis_imp.getRoi()
        fit_basis_imp.killRoi()
        pts = [(pt.x, pt.y) for pt in roi.getContainedPoints()]
        clean_pts = convex_hull_pts(pts)
        clean_pts = [(x, z_xy_ratio * y) for (x, y) in clean_pts]
        # make a stack of clean points...
        ip = FloatProcessor(width, height + 1)
        pix = ip.getPixels()
        for pt in clean_pts:
            pix[int(pt[1]) * width + int(pt[0])] = 128
        pts_stack.addSlice(ip)
        centre, angle, axl = ellipse_fitting.fit_ellipse(clean_pts)
        major_axes.append(max(axl))
        centres.append(centre)
        rot_seg_imp.setZ(zidx + 1)
        ellipse_roi = ellipse_fitting.generate_ellipse_roi(centre, angle, axl)
        rois.append(ellipse_roi)
    IJ.run(imp, "Line Width...", "line=1")
    cal = imp.getCalibration()
    smooth_centres, tangent_vecs = generate_smoothed_vessel_axis(
        centres, pixel_size_um=cal.pixelDepth)
    for zidx in range(fit_basis_imp.getNSlices()):
        centre = smooth_centres[zidx]
        major_axis = major_axes[zidx]
        ellipse_roi = EllipseRoi(centre[0] - 2, centre[1], centre[0] + 2,
                                 centre[1], 1.0)
        roi_imp.setZ(zidx + 1)
        roi_imp.setRoi(ellipse_roi)
        IJ.run(roi_imp, "Set...",
               "value=" + str(roi_imp.getProcessor().maxValue()) + " slice")

    pts_stack_imp = ImagePlus("Cleaned points", pts_stack)
    pts_stack_imp.setTitle("pts_stack_imp")
    pts_stack_imp.show()

    rot_seg_imp.changes = False
    rot_seg_imp.close()
    egfp_imp = egfp_mch_imps[0]
    mch_imp = egfp_mch_imps[1]
    imps_to_combine = [egfp_mch_imps[1], egfp_mch_imps[0], roi_imp]
    egfp_imp.show()
    mch_imp.show()
    roi_imp.show()
    print("box height um = " +
          str(roi_imp.getNSlices() * info.get_xy_pixel_size_um()))
    IJ.run(
        egfp_imp, "Size...", "width=" + str(width) + " height=" + str(height) +
        " depth=" + str(depth) + " average interpolation=Bilinear")
    IJ.run(
        mch_imp, "Size...", "width=" + str(width) + " height=" + str(height) +
        " depth=" + str(depth) + " average interpolation=Bilinear")
    #IJ.run("Merge Channels...", "c1=[" + mch_imp.getTitle() +
    #								"] c2=[" + egfp_imp.getTitle() +
    #								"] c7=[" + roi_imp.getTitle() + "] create keep");
    composite_imp = RGBStackMerge().mergeChannels(imps_to_combine, False)
    print(composite_imp)
    composite_imp.show()
    print("end of vessel centerline id step, image dims = ({}x{}x{})".format(
        composite_imp.getWidth(), composite_imp.getHeight(),
        composite_imp.getNSlices()))
    WaitForUserDialog("pause").show()
    # do qc here?

    #WM.getImage("Composite").addImageListener(UpdateRoiImageListener(rois));
    IJ.run(roi_imp, "8-bit", "")

    if save_output:
        FileSaver(composite_imp).saveAsTiffStack(
            os.path.join(output_path, "segmentation result.tif"))
        print(roi_imp)
        FileSaver(roi_imp).saveAsTiff(
            os.path.join(output_path, "vessel axis.tif"))

    egfp_imp.changes = False
    mch_imp.changes = False
    roi_imp.changes = False
    fit_basis_imp.changes = False
    pts_stack_imp.changes = False
    egfp_imp.close()
    mch_imp.close()
    #roi_imp.close();
    fit_basis_imp.close()
    pts_stack_imp.close()

    zcoords = [i for i in range(composite_imp.getNSlices())]
    xyz_smooth_centres = [(x, y, z)
                          for ((x, y), z) in zip(smooth_centres, zcoords)]

    composite_imp2 = straighten_vessel(composite_imp,
                                       xyz_smooth_centres,
                                       save_output=True)
    composite_imp3 = straighten_vessel(composite_imp2,
                                       xyz_smooth_centres,
                                       it=2,
                                       save_output=True)
    return composite_imp3
def downsample_for_isotropy(imp, extra_downsample_factor=2.0, info=None):
	"""downsample x, y pixel directions to get ~cubic voxels"""
	title = imp.getTitle();
	cal = imp.getCalibration();
	if info is None:
		pix_w = cal.pixelWidth;
		pix_h = cal.pixelHeight;
		pix_d = cal.pixelDepth;
	else:
		pix_w = info.get_xy_pixel_size_um();
		pix_h = pix_w;
		pix_d = info.get_z_plane_spacing_um();
	im_w = imp.getWidth();
	im_h = imp.getHeight();
	im_d = imp.getNSlices();
	print("original pixel whd = ({}, {}, {})".format(pix_w, pix_h, pix_d));
	print("original image whd = ({}, {}, {})".format(im_w, im_h, im_d));
	im_nch = imp.getNChannels();
	if im_nch > 1:
		split_ch = ChannelSplitter().split(imp);
	else:
		split_ch = [imp];
	print("downsampling {} and making isotropic...".format(title));
	IJ.showStatus("Downsampling and making ~isotropic...");
	xy_scale = pix_h / (pix_d * extra_downsample_factor);
	xy_scaled_h = int(xy_scale * im_h);
	xy_scaled_w = int(xy_scale * im_w);
	z_scale = 1/ extra_downsample_factor;
	z_scaled_h = int(z_scale * im_d);
	out_imps = [];
	for ch_imp in split_ch:
		print(ch_imp.getTitle());
		sp = StackProcessor(ch_imp.getStack());
		print((xy_scaled_w, xy_scaled_h));
		stack = sp.resize(xy_scaled_w, xy_scaled_h, True);
		xz_stack = rot_around_x(stack);
		xz_sp = StackProcessor(xz_stack);
		xz_stack = xz_sp.resize(xy_scaled_w, z_scaled_h, True);
		out_stack = rot_around_x(xz_stack);
		out_imps.append(ImagePlus("Isotropic downsampled {}".format(title), out_stack));
	cal.setUnit('um');
	cal.pixelWidth = im_w/xy_scaled_w * pix_w;
	cal.pixelHeight = im_h/xy_scaled_h * pix_h;
	cal.pixelDepth = im_d/z_scaled_h * pix_d;
	print("new pixel whd = ({}, {}, {})".format(cal.pixelWidth, cal.pixelHeight, cal.pixelDepth));
	imp.changes = False;
	imp.close();
	for ch_imp in split_ch:
		ch_imp.close();
	if len(out_imps) > 1:
		out_imp = RGBStackMerge().mergeChannels(out_imps, False);
	else:
		out_imp = out_imps[0];
	out_imp.setCalibration(cal);
	print("new image whd = ({}, {}, {})".format(out_imp.getWidth(), out_imp.getHeight(), out_imp.getNSlices()));
	print("...done downsampling {} and making isotropic. ".format(title));
	IJ.showStatus("...done downsampling and making ~isotropic. ");
	return out_imp;