Builds a JSON-serializable object with information about images.

    Args:
      run: The name of the run.
      tag: The name of the tag the images all belong to.
      sample: The zero-indexed sample of the image for which to retrieve
        information. For instance, setting `sample` to `2` will fetch
        information about only the third image of each batch. Steps with
        fewer than three images will be omitted from the results.

    Returns:
      A list of dictionaries containing the wall time, step, URL, width, and
      height for each image.

def _image_response_for_run(self, run, tag, sample):
    """Builds a JSON-serializable object with information about images.

    Args:
      run: The name of the run.
      tag: The name of the tag the images all belong to.
      sample: The zero-indexed sample of the image for which to retrieve
        information. For instance, setting `sample` to `2` will fetch
        information about only the third image of each batch. Steps with
        fewer than three images will be omitted from the results.

    Returns:
      A list of dictionaries containing the wall time, step, URL, width, and
      height for each image.
    """
    if self._db_connection_provider:
      db = self._db_connection_provider()
      cursor = db.execute(
          '''
          SELECT
            computed_time,
            step,
            CAST (T0.data AS INT) AS width,
            CAST (T1.data AS INT) AS height
          FROM Tensors
          JOIN TensorStrings AS T0
            ON Tensors.rowid = T0.tensor_rowid
          JOIN TensorStrings AS T1
            ON Tensors.rowid = T1.tensor_rowid
          WHERE
            series = (
              SELECT tag_id
              FROM Runs
              CROSS JOIN Tags USING (run_id)
              WHERE Runs.run_name = :run AND Tags.tag_name = :tag)
            AND step IS NOT NULL
            AND dtype = :dtype
            /* Should be n-vector, n >= 3: [width, height, samples...] */
            AND (NOT INSTR(shape, ',') AND CAST (shape AS INT) >= 3)
            AND T0.idx = 0
            AND T1.idx = 1
          ORDER BY step
          ''',
          {'run': run, 'tag': tag, 'dtype': tf.string.as_datatype_enum})
      return [{
          'wall_time': computed_time,
          'step': step,
          'width': width,
          'height': height,
          'query': self._query_for_individual_image(run, tag, sample, index)
      } for index, (computed_time, step, width, height) in enumerate(cursor)]
    response = []
    index = 0
    tensor_events = self._multiplexer.Tensors(run, tag)
    filtered_events = self._filter_by_sample(tensor_events, sample)
    for (index, tensor_event) in enumerate(filtered_events):
      (width, height) = tensor_event.tensor_proto.string_val[:2]
      response.append({
          'wall_time': tensor_event.wall_time,
          'step': tensor_event.step,
          # We include the size so that the frontend can add that to the <img>
          # tag so that the page layout doesn't change when the image loads.
          'width': int(width),
          'height': int(height),
          'query': self._query_for_individual_image(run, tag, sample, index)
      })
    return response