blob: e1ef1309bbeec2a254e9d44b90cfd2342c6e630d [file] [log] [blame]
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
package org.tensorflow.lite.support.image;
import android.graphics.PointF;
import android.graphics.RectF;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import org.tensorflow.lite.support.common.Operator;
import org.tensorflow.lite.support.common.SequentialProcessor;
import org.tensorflow.lite.support.common.SupportPreconditions;
import org.tensorflow.lite.support.common.TensorOperator;
import org.tensorflow.lite.support.image.ops.Rot90Op;
import org.tensorflow.lite.support.image.ops.TensorOperatorWrapper;
/**
* ImageProcessor is a helper class for preprocessing and postprocessing {@link TensorImage}. It
* could transform a {@link TensorImage} to another by executing a chain of {@link ImageOperator}.
*
* <p>Example Usage:
*
* <pre>
* ImageProcessor processor = new ImageProcessor.Builder()
* .add(new ResizeOp(224, 224, ResizeMethod.NEAREST_NEIGHBOR)
* .add(new Rot90Op())
* .add(new NormalizeOp(127.5f, 127.5f))
* .build();
* TensorImage anotherTensorImage = processor.process(tensorImage);
* </pre>
*
* <p><b>WARNING:</b> Instances of an {@code ImageProcessor} are <b>not</b> thread-safe with {@link
* #updateNumberOfRotations}. Updating the number of rotations and then processing images (using
* {@link #process}) must be protected from concurrent access. It is recommended to create separate
* {@code ImageProcessor} instances for each thread. If multiple threads access a {@code
* ImageProcessor} concurrently, it must be synchronized externally.
*
* @see ImageProcessor.Builder to build a {@link ImageProcessor} instance
* @see ImageProcessor#process(TensorImage) to apply the processor on a {@link TensorImage}
*/
public class ImageProcessor extends SequentialProcessor<TensorImage> {
private ImageProcessor(Builder builder) {
super(builder);
}
/**
* Transforms a point from coordinates system of the result image back to the one of the input
* image.
*
* @param point the point from the result coordinates system.
* @param inputImageHeight the height of input image.
* @param inputImageWidth the width of input image.
* @return the point with the coordinates from the coordinates system of the input image.
*/
public PointF inverseTransform(PointF point, int inputImageHeight, int inputImageWidth) {
List<Integer> widths = new ArrayList<>();
List<Integer> heights = new ArrayList<>();
int currentWidth = inputImageWidth;
int currentHeight = inputImageHeight;
for (Operator<TensorImage> op : operatorList) {
widths.add(currentWidth);
heights.add(currentHeight);
ImageOperator imageOperator = (ImageOperator) op;
int newHeight = imageOperator.getOutputImageHeight(currentHeight, currentWidth);
int newWidth = imageOperator.getOutputImageWidth(currentHeight, currentWidth);
currentHeight = newHeight;
currentWidth = newWidth;
}
ListIterator<Operator<TensorImage>> opIterator = operatorList.listIterator(operatorList.size());
ListIterator<Integer> widthIterator = widths.listIterator(widths.size());
ListIterator<Integer> heightIterator = heights.listIterator(heights.size());
while (opIterator.hasPrevious()) {
ImageOperator imageOperator = (ImageOperator) opIterator.previous();
int height = heightIterator.previous();
int width = widthIterator.previous();
point = imageOperator.inverseTransform(point, height, width);
}
return point;
}
/**
* Transforms a rectangle from coordinates system of the result image back to the one of the input
* image.
*
* @param rect the rectangle from the result coordinates system.
* @param inputImageHeight the height of input image.
* @param inputImageWidth the width of input image.
* @return the rectangle with the coordinates from the coordinates system of the input image.
*/
public RectF inverseTransform(RectF rect, int inputImageHeight, int inputImageWidth) {
// when rotation is involved, corner order may change - top left changes to bottom right, .etc
PointF p1 =
inverseTransform(new PointF(rect.left, rect.top), inputImageHeight, inputImageWidth);
PointF p2 =
inverseTransform(new PointF(rect.right, rect.bottom), inputImageHeight, inputImageWidth);
return new RectF(
Math.min(p1.x, p2.x), Math.min(p1.y, p2.y), Math.max(p1.x, p2.x), Math.max(p1.y, p2.y));
}
/**
* The Builder to create an ImageProcessor, which could be executed later.
*
* @see #add(TensorOperator) to add a general TensorOperator
* @see #add(ImageOperator) to add an ImageOperator
* @see #build() complete the building process and get a built Processor
*/
public static class Builder extends SequentialProcessor.Builder<TensorImage> {
public Builder() {
super();
}
/**
* Adds an {@link ImageOperator} into the Operator chain.
*
* @param op the Operator instance to be executed then
*/
public Builder add(ImageOperator op) {
super.add(op);
return this;
}
/**
* Adds a {@link TensorOperator} into the Operator chain. In execution, the processor calls
* {@link TensorImage#getTensorBuffer()} to transform the {@link TensorImage} by transforming
* the underlying {@link org.tensorflow.lite.support.tensorbuffer.TensorBuffer}.
*
* @param op the Operator instance to be executed then
*/
public Builder add(TensorOperator op) {
return add(new TensorOperatorWrapper(op));
}
/** Completes the building process and gets the {@link ImageProcessor} instance. */
@Override
public ImageProcessor build() {
return new ImageProcessor(this);
}
}
/**
* Updates the number of rotations for the first {@link Rot90Op} in this {@link ImageProcessor}.
*
* <p><b>WARNING:</b>this method is <b>not</b> thread-safe. Updating the number of rotations and
* then processing images (using {@link #process}) must be protected from concurrent access with
* additional synchronization.
*
* @param k the number of rotations
* @throws IllegalStateException if {@link Rot90Op} has not been added to this {@link
* ImageProcessor}
*/
public void updateNumberOfRotations(int k) {
updateNumberOfRotations(k, /*occurrence=*/ 0);
}
/**
* Updates the number of rotations for the {@link Rot90Op} specified by {@code occurrence} in this
* {@link ImageProcessor}.
*
* <p><b>WARNING:</b>this method is <b>not</b> thread-safe. Updating the number of rotations and
* then processing images (using {@link #process}) must be protected from concurrent access with
* additional synchronization.
*
* @param k the number of rotations
* @param occurrence the index of perticular {@link Rot90Op} in this {@link ImageProcessor}. For
* example, if the second {@link Rot90Op} needs to be updated, {@code occurrence} should be
* set to 1.
* @throws IndexOutOfBoundsException if {@code occurrence} is negative or is not less than the
* number of {@link Rot90Op} in this {@link ImageProcessor}
* @throws IllegalStateException if {@link Rot90Op} has not been added to this {@link
* ImageProcessor}
*/
public synchronized void updateNumberOfRotations(int k, int occurrence) {
SupportPreconditions.checkState(
operatorIndex.containsKey(Rot90Op.class.getName()),
"The Rot90Op has not been added to the ImageProcessor.");
List<Integer> indexes = operatorIndex.get(Rot90Op.class.getName());
SupportPreconditions.checkElementIndex(occurrence, indexes.size(), "occurrence");
// The index of the Rot90Op to be replaced in operatorList.
int index = indexes.get(occurrence);
Rot90Op newRot = new Rot90Op(k);
operatorList.set(index, newRot);
}
}