private ExtractPSF<FloatType> loadPSFs(
      final Channel ch,
      final ArrayList<ViewDescription> allInputData,
      final HashMap<Channel, ArrayList<Pair<Pair<Angle, Illumination>, String>>> psfFiles,
      final boolean transformLoadedPSFs) {
    final HashMap<ViewId, AffineTransform3D> models;

    if (transformLoadedPSFs) {
      models = new HashMap<ViewId, AffineTransform3D>();

      for (final ViewDescription viewDesc : allInputData)
        models.put(
            viewDesc, spimData.getViewRegistrations().getViewRegistration(viewDesc).getModel());
    } else {
      models = null;
    }

    return ExtractPSF.loadAndTransformPSFs(psfFiles.get(ch), allInputData, new FloatType(), models);
  }
  protected ExtractPSF<FloatType> assignOtherChannel(
      final Channel channel, final HashMap<Channel, ChannelPSF> extractPSFLabels) {
    final ChannelPSF thisChannelPSF = extractPSFLabels.get(channel);
    final ChannelPSF otherChannelPSF = extractPSFLabels.get(thisChannelPSF.getOtherChannel());

    final Channel otherChannel = thisChannelPSF.getOtherChannel();
    for (int i = 0; i < viewDescriptions.size(); ++i) {
      // the viewid to map from
      final ViewDescription sourceVD = viewDescriptions.get(i);

      // search the viewid to map to
      for (final ViewId viewId : viewIdsToProcess) {
        final ViewDescription otherVD =
            spimData.getSequenceDescription().getViewDescription(viewId);

        if (otherVD.getViewSetup().getAngle().getId() == sourceVD.getViewSetup().getAngle().getId()
            && otherVD.getViewSetup().getIllumination().getId()
                == sourceVD.getViewSetup().getIllumination().getId()
            && otherVD.getTimePointId() == sourceVD.getTimePointId()
            && otherVD.getViewSetup().getChannel().getId() == otherChannel.getId()) {
          ePSF.getViewIdMapping().put(sourceVD, otherVD);

          IOFunctions.println(
              "ViewID="
                  + sourceVD.getViewSetupId()
                  + ", TPID="
                  + sourceVD.getTimePointId()
                  + " takes the PSF from "
                  + "ViewID="
                  + otherVD.getViewSetupId()
                  + ", TPID="
                  + otherVD.getTimePointId());
        }
      }
    }

    return otherChannelPSF.getExtractPSFInstance();
  }
  /**
   * Fuses one stack, i.e. all angles/illuminations for one timepoint and channel
   *
   * @param timepoint
   * @param channel
   * @return
   */
  public boolean fuseStacksAndGetPSFs(
      final TimePoint timepoint,
      final Channel channel,
      final ImgFactory<FloatType> imgFactory,
      final int osemIndex,
      double osemspeedup,
      WeightType weightType,
      final HashMap<Channel, ChannelPSF> extractPSFLabels,
      final long[] psfSize,
      final HashMap<Channel, ArrayList<Pair<Pair<Angle, Illumination>, String>>> psfFiles,
      final boolean transformLoadedPSFs) {
    // TODO: get rid of this hack
    if (files != null) {
      weightType = WeightType.LOAD_WEIGHTS;
      IOFunctions.println("WARNING: LOADING WEIGHTS FROM IMAGES, files.length()=" + files.length);
    }

    // get all views that are fused for this timepoint & channel
    this.viewDescriptions =
        FusionHelper.assembleInputData(spimData, timepoint, channel, viewIdsToProcess);

    if (this.viewDescriptions.size() == 0) return false;

    this.imgs = new HashMap<ViewId, RandomAccessibleInterval<FloatType>>();
    this.weights = new HashMap<ViewId, RandomAccessibleInterval<FloatType>>();

    final Img<FloatType> overlapImg;

    if (weightType == WeightType.WEIGHTS_ONLY)
      overlapImg = imgFactory.create(bb.getDimensions(), new FloatType());
    else overlapImg = null;

    final boolean extractPSFs =
        (extractPSFLabels != null) && (extractPSFLabels.get(channel).getLabel() != null);
    final boolean loadPSFs = (psfFiles != null);

    if (extractPSFs) ePSF = new ExtractPSF<FloatType>();
    else if (loadPSFs) ePSF = loadPSFs(channel, viewDescriptions, psfFiles, transformLoadedPSFs);
    else {
      ePSF = assignOtherChannel(channel, extractPSFLabels);
    }

    if (ePSF == null) return false;

    // remember the extracted or loaded PSFs
    extractPSFLabels.get(channel).setExtractPSFInstance(ePSF);

    // we will need to run some batches until all is fused
    for (int i = 0; i < viewDescriptions.size(); ++i) {
      final ViewDescription vd = viewDescriptions.get(i);

      IOFunctions.println(
          "Transforming view "
              + i
              + " of "
              + (viewDescriptions.size() - 1)
              + " (viewsetup="
              + vd.getViewSetupId()
              + ", tp="
              + vd.getTimePointId()
              + ")");
      IOFunctions.println(
          "("
              + new Date(System.currentTimeMillis())
              + "): Reserving memory for transformed & weight image.");

      // creating the output
      RandomAccessibleInterval<FloatType> transformedImg; // might be null if WEIGHTS_ONLY
      final RandomAccessibleInterval<FloatType>
          weightImg; // never null (except LOAD_WEIGHTS which is not implemented yet)

      if (weightType == WeightType.WEIGHTS_ONLY) transformedImg = overlapImg;
      else transformedImg = imgFactory.create(bb.getDimensions(), new FloatType());

      IOFunctions.println(
          "("
              + new Date(System.currentTimeMillis())
              + "): Transformed image factory: "
              + imgFactory.getClass().getSimpleName());

      // loading the input if necessary
      final RandomAccessibleInterval<FloatType> img;

      if (weightType == WeightType.WEIGHTS_ONLY && !extractPSFs) {
        img = null;
      } else {
        IOFunctions.println("(" + new Date(System.currentTimeMillis()) + "): Loading image.");
        img = ProcessFusion.getImage(new FloatType(), spimData, vd, true);

        if (Img.class.isInstance(img))
          IOFunctions.println(
              "("
                  + new Date(System.currentTimeMillis())
                  + "): Input image factory: "
                  + ((Img<FloatType>) img).factory().getClass().getSimpleName());
      }

      // initializing weights
      IOFunctions.println(
          "("
              + new Date(System.currentTimeMillis())
              + "): Initializing transformation & weights: "
              + weightType.name());

      spimData.getViewRegistrations().getViewRegistration(vd).updateModel();
      final AffineTransform3D transform =
          spimData.getViewRegistrations().getViewRegistration(vd).getModel();
      final long[] offset = new long[] {bb.min(0), bb.min(1), bb.min(2)};

      if (weightType == WeightType.PRECOMPUTED_WEIGHTS || weightType == WeightType.WEIGHTS_ONLY)
        weightImg = imgFactory.create(bb.getDimensions(), new FloatType());
      else if (weightType == WeightType.NO_WEIGHTS)
        weightImg =
            Views.interval(
                new ConstantRandomAccessible<FloatType>(
                    new FloatType(1), transformedImg.numDimensions()),
                transformedImg);
      else if (weightType == WeightType.VIRTUAL_WEIGHTS) {
        final Blending blending = getBlending(img, blendingBorder, blendingRange, vd);

        weightImg =
            new TransformedRealRandomAccessibleInterval<FloatType>(
                blending, new FloatType(), transformedImg, transform, offset);
      } else // if ( processType == ProcessType.LOAD_WEIGHTS )
      {
        IOFunctions.println("WARNING: LOADING WEIGHTS FROM: '" + new File(files[i]) + "'");
        ImagePlus imp = StackImgLoaderIJ.open(new File(files[i]));
        weightImg = imgFactory.create(bb.getDimensions(), new FloatType());
        StackImgLoaderIJ.imagePlus2ImgLib2Img(imp, (Img<FloatType>) weightImg, false);
        imp.close();
        if (debugImport) {
          imp = ImageJFunctions.show(weightImg);
          imp.setTitle("ViewSetup " + vd.getViewSetupId() + " Timepoint " + vd.getTimePointId());
        }
      }

      // split up into many parts for multithreading
      final Vector<ImagePortion> portions =
          FusionHelper.divideIntoPortions(
              Views.iterable(transformedImg).size(), Threads.numThreads() * 4);

      // set up executor service
      final ExecutorService taskExecutor = Executors.newFixedThreadPool(Threads.numThreads());
      final ArrayList<Callable<String>> tasks = new ArrayList<Callable<String>>();

      IOFunctions.println(
          "("
              + new Date(System.currentTimeMillis())
              + "): Transforming image & computing weights.");

      for (final ImagePortion portion : portions) {
        if (weightType == WeightType.WEIGHTS_ONLY) {
          final Interval imgInterval =
              new FinalInterval(
                  ViewSetupUtils.getSizeOrLoad(
                      vd.getViewSetup(),
                      vd.getTimePoint(),
                      spimData.getSequenceDescription().getImgLoader()));
          final Blending blending = getBlending(imgInterval, blendingBorder, blendingRange, vd);

          tasks.add(
              new TransformWeights(
                  portion, imgInterval, blending, transform, overlapImg, weightImg, offset));
        } else if (weightType == WeightType.PRECOMPUTED_WEIGHTS) {
          final Blending blending = getBlending(img, blendingBorder, blendingRange, vd);

          tasks.add(
              new TransformInputAndWeights(
                  portion, img, blending, transform, transformedImg, weightImg, offset));
        } else if (weightType == WeightType.NO_WEIGHTS
            || weightType == WeightType.VIRTUAL_WEIGHTS
            || weightType == WeightType.LOAD_WEIGHTS) {
          tasks.add(new TransformInput(portion, img, transform, transformedImg, offset));
        } else {
          throw new RuntimeException(weightType.name() + " not implemented yet.");
        }
      }

      try {
        // invokeAll() returns when all tasks are complete
        taskExecutor.invokeAll(tasks);
      } catch (final InterruptedException e) {
        IOFunctions.println("Failed to compute fusion: " + e);
        e.printStackTrace();
        return false;
      }

      taskExecutor.shutdown();

      // extract PSFs if wanted
      if (extractPSFs) {
        final ArrayList<double[]> llist =
            getLocationsOfCorrespondingBeads(
                timepoint, vd, extractPSFLabels.get(channel).getLabel());

        IOFunctions.println(
            "("
                + new Date(System.currentTimeMillis())
                + "): Extracting PSF for viewsetup "
                + vd.getViewSetupId()
                + " using label '"
                + extractPSFLabels.get(channel).getLabel()
                + "'"
                + " ("
                + llist.size()
                + " corresponding detections available)");

        ePSF.extractNextImg(img, vd, transform, llist, psfSize);
      }

      if (weightType != WeightType.WEIGHTS_ONLY) imgs.put(vd, transformedImg);
      weights.put(vd, weightImg);
    }

    // normalize the weights
    final ArrayList<RandomAccessibleInterval<FloatType>> weightsSorted =
        new ArrayList<RandomAccessibleInterval<FloatType>>();

    for (final ViewDescription vd : viewDescriptions) weightsSorted.add(weights.get(vd));

    IOFunctions.println(
        "("
            + new Date(System.currentTimeMillis())
            + "): Computing weight normalization for deconvolution.");

    final WeightNormalizer wn;

    if (weightType == WeightType.WEIGHTS_ONLY
        || weightType == WeightType.PRECOMPUTED_WEIGHTS
        || weightType == WeightType.LOAD_WEIGHTS) wn = new WeightNormalizer(weightsSorted);
    else if (weightType == WeightType.VIRTUAL_WEIGHTS)
      wn = new WeightNormalizer(weightsSorted, imgFactory);
    else // if ( processType == ProcessType.NO_WEIGHTS )
    wn = null;

    if (wn != null && !wn.process()) return false;

    // put the potentially modified weights back
    for (int i = 0; i < viewDescriptions.size(); ++i)
      weights.put(viewDescriptions.get(i), weightsSorted.get(i));

    this.minOverlappingViews = wn.getMinOverlappingViews();
    this.avgOverlappingViews = wn.getAvgOverlappingViews();

    IOFunctions.println(
        "("
            + new Date(System.currentTimeMillis())
            + "): Minimal number of overlapping views: "
            + getMinOverlappingViews()
            + ", using "
            + (this.minOverlappingViews = Math.max(1, this.minOverlappingViews)));
    IOFunctions.println(
        "("
            + new Date(System.currentTimeMillis())
            + "): Average number of overlapping views: "
            + getAvgOverlappingViews()
            + ", using "
            + (this.avgOverlappingViews = Math.max(1, this.avgOverlappingViews)));

    if (osemIndex == 1) osemspeedup = getMinOverlappingViews();
    else if (osemIndex == 2) osemspeedup = getAvgOverlappingViews();

    IOFunctions.println(
        "("
            + new Date(System.currentTimeMillis())
            + "): Adjusting for OSEM speedup = "
            + osemspeedup);

    if (weightType == WeightType.WEIGHTS_ONLY)
      displayWeights(osemspeedup, weightsSorted, overlapImg, imgFactory);
    else adjustForOSEM(weights, weightType, osemspeedup);

    IOFunctions.println(
        "("
            + new Date(System.currentTimeMillis())
            + "): Finished precomputations for deconvolution.");

    return true;
  }