Q:

Using Parallel. ForEach vs TPL.Dataflow or other solution for applying OCR to large number of images

I'm creating an application to batch OCR images, and right now I'm using an async Parallel. ForEach method to iterate through a List of objects (Printouts) that contain fields for the filename and the OCR'ed text.  
I wonder if this is the best way of going about this.  I've read about TPL.Dataflow and although it looks like overkill I wonder if it's better to use a more sophisticated approach as I'll potentially be processing hundreds of files at once, and I'm not sure if having hundreds of tasks created is good practice.  Also, I've read it's bad practice to use Interlocked. Increment in a Parallel. ForEach, should I be converting this to a Parallel. For?  Below is my current implementation:
private async void BatchOCR_Click(object sender, EventArgs e) {
   //some UI stuff
   source = new CancellationTokenSource();
   progressBar. Value = 0;
   int counter = 0;
   IProgress<int> progress = new Progress<int>(i => { progressBar. Value = (int)Math. Round((float)(i)*100 / fileList. Items. Count, 0); });

   await Task. Run(() => RunBatchOCR(ListOfPrintouts,progress,counter), source. Token);
   //some UI stuff
}

private async Task RunBatchOCR(List<Printout> printouts,IProgress<int> progress, int counter) {
   progress. Report(0);
   Parallel. ForEach(printouts, (printout,state) =>
      {
         try
         {
            source. Token. ThrowIfCancellationRequested();
         }
         catch
         {
            Console. WriteLine("Task was cancelled");
            cancelButton. Enabled = false;
            state. Break();
          }
          finally
          {
             Interlocked. Increment(ref counter);
          }
          printout. OcrHelper.runOCR(); //loads bitmap and extracts text
          progress. Report(counter);
          Console. WriteLine(counter. ToString());
    });
}

A:

I'm not sure if having hundreds of tasks created is good practice

It's fine.  Parallel uses intelligent partitioning. 
Regarding the rest of the code, Interlocked is fine to use as a counter, but you don't want to access the same variable without interlocked barriers.  And that CancellationToken code is begging to be simplified:
private async Task RunBatchOCR(List<Printout> printouts, IProgress<int> progress)
{
  int counter = 0;
  progress?.Report(0);
  try
  {
    Parallel. ForEach(printouts, new ParallelOptions { CancellationToken = source. Token }, printout =>
    {
      printout. OcrHelper.runOCR(); //loads bitmap and extracts text
      var update = Interlocked. Increment(ref counter);
      progress?.Report(update);
      Console. WriteLine(update. ToString());
    });
  }
  catch (OperationCanceledException)
  {
    Console. WriteLine("Task was cancelled");
    cancelButton. Enabled = false;
  }
}

