T[] toArray(T[] array) {
+ if (array.length < mSize) {
+ @SuppressWarnings("unchecked") T[] newArray =
+ (T[]) Array.newInstance(array.getClass().getComponentType(), mSize);
+ array = newArray;
+ }
+ System.arraycopy(mArray, 0, array, 0, mSize);
+ if (array.length > mSize) {
+ array[mSize] = null;
+ }
+ return array;
+ }
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation returns false if the object is not a set, or
+ * if the sets have different sizes. Otherwise, for each value in this
+ * set, it checks to make sure the value also exists in the other set.
+ * If any value doesn't exist, the method returns false; otherwise, it
+ * returns true.
+ */
+ @Override
+ public boolean equals(@Nullable Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof Set) {
+ Set> set = (Set>) object;
+ if (size() != set.size()) {
+ return false;
+ }
+ try {
+ for (int i = 0; i < mSize; i++) {
+ E mine = valueAt(i);
+ if (!set.contains(mine)) {
+ return false;
+ }
+ }
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int[] hashes = mHashes;
+ int result = 0;
+ for (int i = 0, s = mSize; i < s; i++) {
+ result += hashes[i];
+ }
+ return result;
+ }
+ /**
+ * {@inheritDoc}
+ *
+ *
This implementation composes a string by iterating over its values. If
+ * this set contains itself as a value, the string "(this Set)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (isEmpty()) {
+ return "{}";
+ }
+ StringBuilder buffer = new StringBuilder(mSize * 14);
+ buffer.append('{');
+ for (int i = 0; i < mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(value);
+ } else {
+ buffer.append("(this Set)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+ // ------------------------------------------------------------------------
+ // Interop with traditional Java containers. Not as efficient as using
+ // specialized collection APIs.
+ // ------------------------------------------------------------------------
+ private MapCollections getCollection() {
+ if (mCollections == null) {
+ mCollections = new MapCollections() {
+ @Override
+ protected int colGetSize() {
+ return mSize;
+ }
+ @Override
+ protected Object colGetEntry(int index, int offset) {
+ return mArray[index];
+ }
+ @Override
+ protected int colIndexOfKey(Object key) {
+ return indexOf(key);
+ }
+ @Override
+ protected int colIndexOfValue(Object value) {
+ return indexOf(value);
+ }
+ @Override
+ protected Map colGetMap() {
+ throw new UnsupportedOperationException("not a map");
+ }
+ @Override
+ protected void colPut(E key, E value) {
+ add(key);
+ }
+ @Override
+ protected E colSetValue(int index, E value) {
+ throw new UnsupportedOperationException("not a map");
+ }
+ @Override
+ protected void colRemoveAt(int index) {
+ removeAt(index);
+ }
+ @Override
+ protected void colClear() {
+ clear();
+ }
+ };
+ }
+ return mCollections;
+ }
+ /**
+ * Return an {@link java.util.Iterator} over all values in the set.
+ *
+ * Note: this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.
+ */
+ @Override
+ public Iterator iterator() {
+ return getCollection().getKeySet().iterator();
+ }
+ /**
+ * Determine if the array set contains all of the values in the given collection.
+ * @param collection The collection whose contents are to be checked against.
+ * @return Returns true if this array set contains a value for every entry
+ * in collection, else returns false.
+ */
+ @Override
+ public boolean containsAll(Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+ /**
+ * Perform an {@link #add(Object)} of all values in collection
+ * @param collection The collection whose contents are to be retrieved.
+ */
+ @Override
+ public boolean addAll(Collection extends E> collection) {
+ ensureCapacity(mSize + collection.size());
+ boolean added = false;
+ for (E value : collection) {
+ added |= add(value);
+ }
+ return added;
+ }
+ /**
+ * Remove all values in the array set that exist in the given collection.
+ * @param collection The collection whose contents are to be used to remove values.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ @Override
+ public boolean removeAll(Collection> collection) {
+ boolean removed = false;
+ for (Object value : collection) {
+ removed |= remove(value);
+ }
+ return removed;
+ }
+ /**
+ * Remove all values in the array set that do not exist in the given collection.
+ * @param collection The collection whose contents are to be used to determine which
+ * values to keep.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ @Override
+ public boolean retainAll(Collection> collection) {
+ boolean removed = false;
+ for (int i = mSize - 1; i >= 0; i--) {
+ if (!collection.contains(mArray[i])) {
+ removeAt(i);
+ removed = true;
+ }
+ }
+ return removed;
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/android/util/ContainerHelpers.java b/Common/src/main/java/android/util/ContainerHelpers.java
new file mode 100644
index 00000000..de76e430
--- /dev/null
+++ b/Common/src/main/java/android/util/ContainerHelpers.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 android.util;
+class ContainerHelpers {
+ // This is Arrays.binarySearch(), but doesn't do any argument validation.
+ static int binarySearch(int[] array, int size, int value) {
+ int lo = 0;
+ int hi = size - 1;
+ while (lo <= hi) {
+ final int mid = (lo + hi) >>> 1;
+ final int midVal = array[mid];
+ if (midVal < value) {
+ lo = mid + 1;
+ } else if (midVal > value) {
+ hi = mid - 1;
+ } else {
+ return mid; // value found
+ }
+ }
+ return ~lo; // value not present
+ }
+ static int binarySearch(long[] array, int size, long value) {
+ int lo = 0;
+ int hi = size - 1;
+ while (lo <= hi) {
+ final int mid = (lo + hi) >>> 1;
+ final long midVal = array[mid];
+ if (midVal < value) {
+ lo = mid + 1;
+ } else if (midVal > value) {
+ hi = mid - 1;
+ } else {
+ return mid; // value found
+ }
+ }
+ return ~lo; // value not present
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/android/util/Half.java b/Common/src/main/java/android/util/Half.java
new file mode 100644
index 00000000..8ac29a99
--- /dev/null
+++ b/Common/src/main/java/android/util/Half.java
@@ -0,0 +1,847 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.util;
+import android.annotation.HalfFloat;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import libcore.util.FP16;
+/**
+ * The {@code Half} class is a wrapper and a utility class to manipulate half-precision 16-bit
+ * IEEE 754
+ * floating point data types (also called fp16 or binary16). A half-precision float can be
+ * created from or converted to single-precision floats, and is stored in a short data type.
+ * To distinguish short values holding half-precision floats from regular short values,
+ * it is recommended to use the @HalfFloat
annotation.
+ *
+ * The IEEE 754 standard specifies an fp16 as having the following format:
+ *
+ * - Sign bit: 1 bit
+ * - Exponent width: 5 bits
+ * - Significand: 10 bits
+ *
+ *
+ * The format is laid out as follows:
+ *
+ * 1 11111 1111111111
+ * ^ --^-- -----^----
+ * sign | |_______ significand
+ * |
+ * -- exponent
+ *
+ *
+ * Half-precision floating points can be useful to save memory and/or
+ * bandwidth at the expense of range and precision when compared to single-precision
+ * floating points (fp32).
+ * To help you decide whether fp16 is the right storage type for you need, please
+ * refer to the table below that shows the available precision throughout the range of
+ * possible values. The precision column indicates the step size between two
+ * consecutive numbers in a specific part of the range.
+ *
+ *
+ * Range start | Precision |
+ * 0 | 1 ⁄ 16,777,216 |
+ * 1 ⁄ 16,384 | 1 ⁄ 16,777,216 |
+ * 1 ⁄ 8,192 | 1 ⁄ 8,388,608 |
+ * 1 ⁄ 4,096 | 1 ⁄ 4,194,304 |
+ * 1 ⁄ 2,048 | 1 ⁄ 2,097,152 |
+ * 1 ⁄ 1,024 | 1 ⁄ 1,048,576 |
+ * 1 ⁄ 512 | 1 ⁄ 524,288 |
+ * 1 ⁄ 256 | 1 ⁄ 262,144 |
+ * 1 ⁄ 128 | 1 ⁄ 131,072 |
+ * 1 ⁄ 64 | 1 ⁄ 65,536 |
+ * 1 ⁄ 32 | 1 ⁄ 32,768 |
+ * 1 ⁄ 16 | 1 ⁄ 16,384 |
+ * 1 ⁄ 8 | 1 ⁄ 8,192 |
+ * 1 ⁄ 4 | 1 ⁄ 4,096 |
+ * 1 ⁄ 2 | 1 ⁄ 2,048 |
+ * 1 | 1 ⁄ 1,024 |
+ * 2 | 1 ⁄ 512 |
+ * 4 | 1 ⁄ 256 |
+ * 8 | 1 ⁄ 128 |
+ * 16 | 1 ⁄ 64 |
+ * 32 | 1 ⁄ 32 |
+ * 64 | 1 ⁄ 16 |
+ * 128 | 1 ⁄ 8 |
+ * 256 | 1 ⁄ 4 |
+ * 512 | 1 ⁄ 2 |
+ * 1,024 | 1 |
+ * 2,048 | 2 |
+ * 4,096 | 4 |
+ * 8,192 | 8 |
+ * 16,384 | 16 |
+ * 32,768 | 32 |
+ *
+ *
+ * This table shows that numbers higher than 1024 lose all fractional precision.
+ */
+@SuppressWarnings("SimplifiableIfStatement")
+public final class Half extends Number implements Comparable {
+ /**
+ * The number of bits used to represent a half-precision float value.
+ */
+ public static final int SIZE = 16;
+ /**
+ * Epsilon is the difference between 1.0 and the next value representable
+ * by a half-precision floating-point.
+ */
+ public static final @HalfFloat short EPSILON = (short) 0x1400;
+ /**
+ * Maximum exponent a finite half-precision float may have.
+ */
+ public static final int MAX_EXPONENT = 15;
+ /**
+ * Minimum exponent a normalized half-precision float may have.
+ */
+ public static final int MIN_EXPONENT = -14;
+ /**
+ * Smallest negative value a half-precision float may have.
+ */
+ public static final @HalfFloat short LOWEST_VALUE = (short) 0xfbff;
+ /**
+ * Maximum positive finite value a half-precision float may have.
+ */
+ public static final @HalfFloat short MAX_VALUE = (short) 0x7bff;
+ /**
+ * Smallest positive normal value a half-precision float may have.
+ */
+ public static final @HalfFloat short MIN_NORMAL = (short) 0x0400;
+ /**
+ * Smallest positive non-zero value a half-precision float may have.
+ */
+ public static final @HalfFloat short MIN_VALUE = (short) 0x0001;
+ /**
+ * A Not-a-Number representation of a half-precision float.
+ */
+ public static final @HalfFloat short NaN = (short) 0x7e00;
+ /**
+ * Negative infinity of type half-precision float.
+ */
+ public static final @HalfFloat short NEGATIVE_INFINITY = (short) 0xfc00;
+ /**
+ * Negative 0 of type half-precision float.
+ */
+ public static final @HalfFloat short NEGATIVE_ZERO = (short) 0x8000;
+ /**
+ * Positive infinity of type half-precision float.
+ */
+ public static final @HalfFloat short POSITIVE_INFINITY = (short) 0x7c00;
+ /**
+ * Positive 0 of type half-precision float.
+ */
+ public static final @HalfFloat short POSITIVE_ZERO = (short) 0x0000;
+ private final @HalfFloat short mValue;
+ /**
+ * Constructs a newly allocated {@code Half} object that represents the
+ * half-precision float type argument.
+ *
+ * @param value The value to be represented by the {@code Half}
+ */
+ public Half(@HalfFloat short value) {
+ mValue = value;
+ }
+ /**
+ * Constructs a newly allocated {@code Half} object that represents the
+ * argument converted to a half-precision float.
+ *
+ * @param value The value to be represented by the {@code Half}
+ *
+ * @see #toHalf(float)
+ */
+ public Half(float value) {
+ mValue = toHalf(value);
+ }
+ /**
+ * Constructs a newly allocated {@code Half} object that
+ * represents the argument converted to a half-precision float.
+ *
+ * @param value The value to be represented by the {@code Half}
+ *
+ * @see #toHalf(float)
+ */
+ public Half(double value) {
+ mValue = toHalf((float) value);
+ }
+ /**
+ * Constructs a newly allocated {@code Half} object that represents the
+ * half-precision float value represented by the string.
+ * The string is converted to a half-precision float value as if by the
+ * {@link #valueOf(String)} method.
+ *
+ * Calling this constructor is equivalent to calling:
+ *
+ * new Half(Float.parseFloat(value))
+ *
+ *
+ * @param value A string to be converted to a {@code Half}
+ * @throws NumberFormatException if the string does not contain a parsable number
+ *
+ * @see Float#valueOf(java.lang.String)
+ * @see #toHalf(float)
+ */
+ public Half(@NonNull String value) throws NumberFormatException {
+ mValue = toHalf(Float.parseFloat(value));
+ }
+ /**
+ * Returns the half-precision value of this {@code Half} as a {@code short}
+ * containing the bit representation described in {@link Half}.
+ *
+ * @return The half-precision float value represented by this object
+ */
+ public @HalfFloat short halfValue() {
+ return mValue;
+ }
+ /**
+ * Returns the value of this {@code Half} as a {@code byte} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code byte}
+ */
+ @Override
+ public byte byteValue() {
+ return (byte) toFloat(mValue);
+ }
+ /**
+ * Returns the value of this {@code Half} as a {@code short} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code short}
+ */
+ @Override
+ public short shortValue() {
+ return (short) toFloat(mValue);
+ }
+ /**
+ * Returns the value of this {@code Half} as a {@code int} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code int}
+ */
+ @Override
+ public int intValue() {
+ return (int) toFloat(mValue);
+ }
+ /**
+ * Returns the value of this {@code Half} as a {@code long} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code long}
+ */
+ @Override
+ public long longValue() {
+ return (long) toFloat(mValue);
+ }
+ /**
+ * Returns the value of this {@code Half} as a {@code float} after
+ * a widening primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code float}
+ */
+ @Override
+ public float floatValue() {
+ return toFloat(mValue);
+ }
+ /**
+ * Returns the value of this {@code Half} as a {@code double} after
+ * a widening primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code double}
+ */
+ @Override
+ public double doubleValue() {
+ return toFloat(mValue);
+ }
+ /**
+ * Returns true if this {@code Half} value represents a Not-a-Number,
+ * false otherwise.
+ *
+ * @return True if the value is a NaN, false otherwise
+ */
+ public boolean isNaN() {
+ return isNaN(mValue);
+ }
+ /**
+ * Compares this object against the specified object. The result is {@code true}
+ * if and only if the argument is not {@code null} and is a {@code Half} object
+ * that represents the same half-precision value as the this object. Two
+ * half-precision values are considered to be the same if and only if the method
+ * {@link #halfToIntBits(short)} returns an identical {@code int} value for both.
+ *
+ * @param o The object to compare
+ * @return True if the objects are the same, false otherwise
+ *
+ * @see #halfToIntBits(short)
+ */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return (o instanceof Half) &&
+ (halfToIntBits(((Half) o).mValue) == halfToIntBits(mValue));
+ }
+ /**
+ * Returns a hash code for this {@code Half} object. The result is the
+ * integer bit representation, exactly as produced by the method
+ * {@link #halfToIntBits(short)}, of the primitive half-precision float
+ * value represented by this {@code Half} object.
+ *
+ * @return A hash code value for this object
+ */
+ @Override
+ public int hashCode() {
+ return hashCode(mValue);
+ }
+ /**
+ * Returns a string representation of the specified half-precision
+ * float value. See {@link #toString(short)} for more information.
+ *
+ * @return A string representation of this {@code Half} object
+ */
+ @NonNull
+ @Override
+ public String toString() {
+ return toString(mValue);
+ }
+ /**
+ * Compares the two specified half-precision float values. The following
+ * conditions apply during the comparison:
+ *
+ *
+ * - {@link #NaN} is considered by this method to be equal to itself and greater
+ * than all other half-precision float values (including {@code #POSITIVE_INFINITY})
+ * - {@link #POSITIVE_ZERO} is considered by this method to be greater than
+ * {@link #NEGATIVE_ZERO}.
+ *
+ *
+ * @param h The half-precision float value to compare to the half-precision value
+ * represented by this {@code Half} object
+ *
+ * @return The value {@code 0} if {@code x} is numerically equal to {@code y}; a
+ * value less than {@code 0} if {@code x} is numerically less than {@code y};
+ * and a value greater than {@code 0} if {@code x} is numerically greater
+ * than {@code y}
+ */
+ @Override
+ public int compareTo(@NonNull Half h) {
+ return compare(mValue, h.mValue);
+ }
+ /**
+ * Returns a hash code for a half-precision float value.
+ *
+ * @param h The value to hash
+ *
+ * @return A hash code value for a half-precision float value
+ */
+ public static int hashCode(@HalfFloat short h) {
+ return halfToIntBits(h);
+ }
+ /**
+ * Compares the two specified half-precision float values. The following
+ * conditions apply during the comparison:
+ *
+ *
+ * - {@link #NaN} is considered by this method to be equal to itself and greater
+ * than all other half-precision float values (including {@code #POSITIVE_INFINITY})
+ * - {@link #POSITIVE_ZERO} is considered by this method to be greater than
+ * {@link #NEGATIVE_ZERO}.
+ *
+ *
+ * @param x The first half-precision float value to compare.
+ * @param y The second half-precision float value to compare
+ *
+ * @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a
+ * value less than {@code 0} if {@code x} is numerically less than {@code y},
+ * and a value greater than {@code 0} if {@code x} is numerically greater
+ * than {@code y}
+ */
+ public static int compare(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.compare(x, y);
+ }
+ /**
+ * Returns a representation of the specified half-precision float value
+ * according to the bit layout described in {@link Half}.
+ *
+ * Similar to {@link #halfToIntBits(short)}, this method collapses all
+ * possible Not-a-Number values to a single canonical Not-a-Number value
+ * defined by {@link #NaN}.
+ *
+ * @param h A half-precision float value
+ * @return The bits that represent the half-precision float value
+ *
+ * @see #halfToIntBits(short)
+ */
+ public static @HalfFloat short halfToShortBits(@HalfFloat short h) {
+ return (h & FP16.EXPONENT_SIGNIFICAND_MASK) > FP16.POSITIVE_INFINITY ? NaN : h;
+ }
+ /**
+ * Returns a representation of the specified half-precision float value
+ * according to the bit layout described in {@link Half}.
+ *
+ * Unlike {@link #halfToRawIntBits(short)}, this method collapses all
+ * possible Not-a-Number values to a single canonical Not-a-Number value
+ * defined by {@link #NaN}.
+ *
+ * @param h A half-precision float value
+ * @return The bits that represent the half-precision float value
+ *
+ * @see #halfToRawIntBits(short)
+ * @see #halfToShortBits(short)
+ * @see #intBitsToHalf(int)
+ */
+ public static int halfToIntBits(@HalfFloat short h) {
+ return (h & FP16.EXPONENT_SIGNIFICAND_MASK) > FP16.POSITIVE_INFINITY ? NaN : h & 0xffff;
+ }
+ /**
+ * Returns a representation of the specified half-precision float value
+ * according to the bit layout described in {@link Half}.
+ *
+ * The argument is considered to be a representation of a half-precision
+ * float value according to the bit layout described in {@link Half}. The 16
+ * most significant bits of the returned value are set to 0.
+ *
+ * @param h A half-precision float value
+ * @return The bits that represent the half-precision float value
+ *
+ * @see #halfToIntBits(short)
+ * @see #intBitsToHalf(int)
+ */
+ public static int halfToRawIntBits(@HalfFloat short h) {
+ return h & 0xffff;
+ }
+ /**
+ * Returns the half-precision float value corresponding to a given
+ * bit representation.
+ *
+ * The argument is considered to be a representation of a half-precision
+ * float value according to the bit layout described in {@link Half}. The 16
+ * most significant bits of the argument are ignored.
+ *
+ * @param bits An integer
+ * @return The half-precision float value with the same bit pattern
+ */
+ public static @HalfFloat short intBitsToHalf(int bits) {
+ return (short) (bits & 0xffff);
+ }
+ /**
+ * Returns the first parameter with the sign of the second parameter.
+ * This method treats NaNs as having a sign.
+ *
+ * @param magnitude A half-precision float value providing the magnitude of the result
+ * @param sign A half-precision float value providing the sign of the result
+ * @return A value with the magnitude of the first parameter and the sign
+ * of the second parameter
+ */
+ public static @HalfFloat short copySign(@HalfFloat short magnitude, @HalfFloat short sign) {
+ return (short) ((sign & FP16.SIGN_MASK) | (magnitude & FP16.EXPONENT_SIGNIFICAND_MASK));
+ }
+ /**
+ * Returns the absolute value of the specified half-precision float.
+ * Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is positive zero (see {@link #POSITIVE_ZERO})
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is positive infinity (see {@link #POSITIVE_INFINITY})
+ *
+ *
+ * @param h A half-precision float value
+ * @return The absolute value of the specified half-precision float
+ */
+ public static @HalfFloat short abs(@HalfFloat short h) {
+ return (short) (h & FP16.EXPONENT_SIGNIFICAND_MASK);
+ }
+ /**
+ * Returns the closest integral half-precision float value to the specified
+ * half-precision float value. Special values are handled in the
+ * following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ *
+ * Note: Unlike the identically named
+ * int java.lang.Math.round(float)
method,
+ * this returns a Half value stored in a short, not an
+ * actual short integer result.
+ *
+ * @param h A half-precision float value
+ * @return The value of the specified half-precision float rounded to the nearest
+ * half-precision float value
+ */
+ public static @HalfFloat short round(@HalfFloat short h) {
+ return FP16.rint(h);
+ }
+ /**
+ * Returns the smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value
+ */
+ public static @HalfFloat short ceil(@HalfFloat short h) {
+ return FP16.ceil(h);
+ }
+ /**
+ * Returns the largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value
+ */
+ public static @HalfFloat short floor(@HalfFloat short h) {
+ return FP16.floor(h);
+ }
+ /**
+ * Returns the truncated half-precision float value of the specified
+ * half-precision float value. Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The truncated half-precision float value of the specified
+ * half-precision float value
+ */
+ public static @HalfFloat short trunc(@HalfFloat short h) {
+ return FP16.trunc(h);
+ }
+ /**
+ * Returns the smaller of two half-precision float values (the value closest
+ * to negative infinity). Special values are handled in the following ways:
+ *
+ * - If either value is NaN, the result is NaN
+ * - {@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}
+ *
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ * @return The smaller of the two specified half-precision values
+ */
+ public static @HalfFloat short min(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.min(x, y);
+ }
+ /**
+ * Returns the larger of two half-precision float values (the value closest
+ * to positive infinity). Special values are handled in the following ways:
+ *
+ * - If either value is NaN, the result is NaN
+ * - {@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}
+ *
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return The larger of the two specified half-precision values
+ */
+ public static @HalfFloat short max(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.max(x, y);
+ }
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than y, false otherwise
+ */
+ public static boolean less(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.less(x, y);
+ }
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than or equal to the second half-precision
+ * float value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than or equal to y, false otherwise
+ */
+ public static boolean lessEquals(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.lessEquals(x, y);
+ }
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ */
+ public static boolean greater(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.greater(x, y);
+ }
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than or equal to the second half-precision float
+ * value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ */
+ public static boolean greaterEquals(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.greaterEquals(x, y);
+ }
+ /**
+ * Returns true if the two half-precision float values are equal.
+ * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO}
+ * and {@link #NEGATIVE_ZERO} are considered equal.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is equal to y, false otherwise
+ */
+ public static boolean equals(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.equals(x, y);
+ }
+ /**
+ * Returns the sign of the specified half-precision float.
+ *
+ * @param h A half-precision float value
+ * @return 1 if the value is positive, -1 if the value is negative
+ */
+ public static int getSign(@HalfFloat short h) {
+ return (h & FP16.SIGN_MASK) == 0 ? 1 : -1;
+ }
+ /**
+ * Returns the unbiased exponent used in the representation of
+ * the specified half-precision float value. if the value is NaN
+ * or infinite, this* method returns {@link #MAX_EXPONENT} + 1.
+ * If the argument is 0 or a subnormal representation, this method
+ * returns {@link #MIN_EXPONENT} - 1.
+ *
+ * @param h A half-precision float value
+ * @return The unbiased exponent of the specified value
+ */
+ public static int getExponent(@HalfFloat short h) {
+ return ((h >>> FP16.EXPONENT_SHIFT) & FP16.SHIFTED_EXPONENT_MASK) - FP16.EXPONENT_BIAS;
+ }
+ /**
+ * Returns the significand, or mantissa, used in the representation
+ * of the specified half-precision float value.
+ *
+ * @param h A half-precision float value
+ * @return The significand, or significand, of the specified vlaue
+ */
+ public static int getSignificand(@HalfFloat short h) {
+ return h & FP16.SIGNIFICAND_MASK;
+ }
+ /**
+ * Returns true if the specified half-precision float value represents
+ * infinity, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is positive infinity or negative infinity,
+ * false otherwise
+ */
+ public static boolean isInfinite(@HalfFloat short h) {
+ return FP16.isInfinite(h);
+ }
+ /**
+ * Returns true if the specified half-precision float value represents
+ * a Not-a-Number, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is a NaN, false otherwise
+ */
+ public static boolean isNaN(@HalfFloat short h) {
+ return FP16.isNaN(h);
+ }
+ /**
+ * Returns true if the specified half-precision float value is normalized
+ * (does not have a subnormal representation). If the specified value is
+ * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY},
+ * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal
+ * number, this method returns false.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is normalized, false otherwise
+ */
+ public static boolean isNormalized(@HalfFloat short h) {
+ return FP16.isNormalized(h);
+ }
+ /**
+ * Converts the specified half-precision float value into a
+ * single-precision float value. The following special cases are handled:
+ *
+ * - If the input is {@link #NaN}, the returned value is {@link Float#NaN}
+ * - If the input is {@link #POSITIVE_INFINITY} or
+ * {@link #NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}
+ * - If the input is 0 (positive or negative), the returned value is +/-0.0f
+ * - Otherwise, the returned value is a normalized single-precision float value
+ *
+ *
+ * @param h The half-precision float value to convert to single-precision
+ * @return A normalized single-precision float value
+ */
+ public static float toFloat(@HalfFloat short h) {
+ return FP16.toFloat(h);
+ }
+ /**
+ * Converts the specified single-precision float value into a
+ * half-precision float value. The following special cases are handled:
+ *
+ * - If the input is NaN (see {@link Float#isNaN(float)}), the returned
+ * value is {@link #NaN}
+ * - If the input is {@link Float#POSITIVE_INFINITY} or
+ * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}
+ * - If the input is 0 (positive or negative), the returned value is
+ * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
+ * - If the input is a less than {@link #MIN_VALUE}, the returned value
+ * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
+ * - If the input is a less than {@link #MIN_NORMAL}, the returned value
+ * is a denorm half-precision float
+ * - Otherwise, the returned value is rounded to the nearest
+ * representable half-precision float value
+ *
+ *
+ * @param f The single-precision float value to convert to half-precision
+ * @return A half-precision float value
+ */
+ @SuppressWarnings("StatementWithEmptyBody")
+ public static @HalfFloat short toHalf(float f) {
+ return FP16.toHalf(f);
+ }
+ /**
+ * Returns a {@code Half} instance representing the specified
+ * half-precision float value.
+ *
+ * @param h A half-precision float value
+ * @return a {@code Half} instance representing {@code h}
+ */
+ public static @NonNull Half valueOf(@HalfFloat short h) {
+ return new Half(h);
+ }
+ /**
+ * Returns a {@code Half} instance representing the specified float value.
+ *
+ * @param f A float value
+ * @return a {@code Half} instance representing {@code f}
+ */
+ public static @NonNull Half valueOf(float f) {
+ return new Half(f);
+ }
+ /**
+ * Returns a {@code Half} instance representing the specified string value.
+ * Calling this method is equivalent to calling
+ * toHalf(Float.parseString(h))
. See {@link Float#valueOf(String)}
+ * for more information on the format of the string representation.
+ *
+ * @param s The string to be parsed
+ * @return a {@code Half} instance representing {@code h}
+ * @throws NumberFormatException if the string does not contain a parsable
+ * half-precision float value
+ */
+ public static @NonNull Half valueOf(@NonNull String s) {
+ return new Half(s);
+ }
+ /**
+ * Returns the half-precision float value represented by the specified string.
+ * Calling this method is equivalent to calling
+ * toHalf(Float.parseString(h))
. See {@link Float#valueOf(String)}
+ * for more information on the format of the string representation.
+ *
+ * @param s The string to be parsed
+ * @return A half-precision float value represented by the string
+ * @throws NumberFormatException if the string does not contain a parsable
+ * half-precision float value
+ */
+ public static @HalfFloat short parseHalf(@NonNull String s) throws NumberFormatException {
+ return toHalf(Float.parseFloat(s));
+ }
+ /**
+ * Returns a string representation of the specified half-precision
+ * float value. Calling this method is equivalent to calling
+ * Float.toString(toFloat(h))
. See {@link Float#toString(float)}
+ * for more information on the format of the string representation.
+ *
+ * @param h A half-precision float value
+ * @return A string representation of the specified value
+ */
+ @NonNull
+ public static String toString(@HalfFloat short h) {
+ return Float.toString(toFloat(h));
+ }
+ /**
+ * Returns a hexadecimal string representation of the specified half-precision
+ * float value. If the value is a NaN, the result is "NaN"
,
+ * otherwise the result follows this format:
+ *
+ * - If the sign is positive, no sign character appears in the result
+ * - If the sign is negative, the first character is
'-'
+ * - If the value is inifinity, the string is
"Infinity"
+ * - If the value is 0, the string is
"0x0.0p0"
+ * - If the value has a normalized representation, the exponent and
+ * significand are represented in the string in two fields. The significand
+ * starts with
"0x1."
followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by "p"
, itself followed by a decimal
+ * string of the unbiased exponent
+ * - If the value has a subnormal representation, the significand starts
+ * with
"0x0."
followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by "p-14"
+ *
+ *
+ * @param h A half-precision float value
+ * @return A hexadecimal string representation of the specified value
+ */
+ @NonNull
+ public static String toHexString(@HalfFloat short h) {
+ return FP16.toHexString(h);
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/android/util/MapCollections.java b/Common/src/main/java/android/util/MapCollections.java
new file mode 100644
index 00000000..f15afcec
--- /dev/null
+++ b/Common/src/main/java/android/util/MapCollections.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 android.util;
+import android.annotation.Nullable;
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+/**
+ * Helper for writing standard Java collection interfaces to a data
+ * structure like {link ArrayMap}.
+ * @hide
+ */
+abstract class MapCollections {
+ EntrySet mEntrySet;
+ KeySet mKeySet;
+ ValuesCollection mValues;
+ final class ArrayIterator implements Iterator {
+ final int mOffset;
+ int mSize;
+ int mIndex;
+ boolean mCanRemove = false;
+ ArrayIterator(int offset) {
+ mOffset = offset;
+ mSize = colGetSize();
+ }
+ @Override
+ public boolean hasNext() {
+ return mIndex < mSize;
+ }
+ @Override
+ public T next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ Object res = colGetEntry(mIndex, mOffset);
+ mIndex++;
+ mCanRemove = true;
+ return (T)res;
+ }
+ @Override
+ public void remove() {
+ if (!mCanRemove) {
+ throw new IllegalStateException();
+ }
+ mIndex--;
+ mSize--;
+ mCanRemove = false;
+ colRemoveAt(mIndex);
+ }
+ }
+ final class MapIterator implements Iterator>, Map.Entry {
+ int mEnd;
+ int mIndex;
+ boolean mEntryValid = false;
+ MapIterator() {
+ mEnd = colGetSize() - 1;
+ mIndex = -1;
+ }
+ @Override
+ public boolean hasNext() {
+ return mIndex < mEnd;
+ }
+ @Override
+ public Map.Entry next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ mIndex++;
+ mEntryValid = true;
+ return this;
+ }
+ @Override
+ public void remove() {
+ if (!mEntryValid) {
+ throw new IllegalStateException();
+ }
+ colRemoveAt(mIndex);
+ mIndex--;
+ mEnd--;
+ mEntryValid = false;
+ }
+ @Override
+ public K getKey() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return (K)colGetEntry(mIndex, 0);
+ }
+ @Override
+ public V getValue() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return (V)colGetEntry(mIndex, 1);
+ }
+ @Override
+ public V setValue(V object) {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return colSetValue(mIndex, object);
+ }
+ @Override
+ public final boolean equals(Object o) {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ if (!(o instanceof Map.Entry)) {
+ return false;
+ }
+ Map.Entry, ?> e = (Map.Entry, ?>) o;
+ return Objects.equals(e.getKey(), colGetEntry(mIndex, 0))
+ && Objects.equals(e.getValue(), colGetEntry(mIndex, 1));
+ }
+ @Override
+ public final int hashCode() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ final Object key = colGetEntry(mIndex, 0);
+ final Object value = colGetEntry(mIndex, 1);
+ return (key == null ? 0 : key.hashCode()) ^
+ (value == null ? 0 : value.hashCode());
+ }
+ @Override
+ public final String toString() {
+ return getKey() + "=" + getValue();
+ }
+ }
+ final class EntrySet implements Set> {
+ @Override
+ public boolean add(Map.Entry object) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean addAll(Collection extends Map.Entry> collection) {
+ int oldSize = colGetSize();
+ for (Map.Entry entry : collection) {
+ colPut(entry.getKey(), entry.getValue());
+ }
+ return oldSize != colGetSize();
+ }
+ @Override
+ public void clear() {
+ colClear();
+ }
+ @Override
+ public boolean contains(Object o) {
+ if (!(o instanceof Map.Entry))
+ return false;
+ Map.Entry, ?> e = (Map.Entry, ?>) o;
+ int index = colIndexOfKey(e.getKey());
+ if (index < 0) {
+ return false;
+ }
+ Object foundVal = colGetEntry(index, 1);
+ return Objects.equals(foundVal, e.getValue());
+ }
+ @Override
+ public boolean containsAll(Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+ @Override
+ public Iterator> iterator() {
+ return new MapIterator();
+ }
+ @Override
+ public boolean remove(Object object) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean removeAll(Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean retainAll(Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+ @Override
+ public Object[] toArray() {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public T[] toArray(T[] array) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean equals(@Nullable Object object) {
+ return equalsSetHelper(this, object);
+ }
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (int i=colGetSize()-1; i>=0; i--) {
+ final Object key = colGetEntry(i, 0);
+ final Object value = colGetEntry(i, 1);
+ result += ( (key == null ? 0 : key.hashCode()) ^
+ (value == null ? 0 : value.hashCode()) );
+ }
+ return result;
+ }
+ };
+ final class KeySet implements Set {
+ @Override
+ public boolean add(K object) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean addAll(Collection extends K> collection) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void clear() {
+ colClear();
+ }
+ @Override
+ public boolean contains(Object object) {
+ return colIndexOfKey(object) >= 0;
+ }
+ @Override
+ public boolean containsAll(Collection> collection) {
+ return containsAllHelper(colGetMap(), collection);
+ }
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+ @Override
+ public Iterator iterator() {
+ return new ArrayIterator(0);
+ }
+ @Override
+ public boolean remove(Object object) {
+ int index = colIndexOfKey(object);
+ if (index >= 0) {
+ colRemoveAt(index);
+ return true;
+ }
+ return false;
+ }
+ @Override
+ public boolean removeAll(Collection> collection) {
+ return removeAllHelper(colGetMap(), collection);
+ }
+ @Override
+ public boolean retainAll(Collection> collection) {
+ return retainAllHelper(colGetMap(), collection);
+ }
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+ @Override
+ public Object[] toArray() {
+ return toArrayHelper(0);
+ }
+ @Override
+ public T[] toArray(T[] array) {
+ return toArrayHelper(array, 0);
+ }
+ @Override
+ public boolean equals(@Nullable Object object) {
+ return equalsSetHelper(this, object);
+ }
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (int i=colGetSize()-1; i>=0; i--) {
+ Object obj = colGetEntry(i, 0);
+ result += obj == null ? 0 : obj.hashCode();
+ }
+ return result;
+ }
+ };
+ final class ValuesCollection implements Collection {
+ @Override
+ public boolean add(V object) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean addAll(Collection extends V> collection) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void clear() {
+ colClear();
+ }
+ @Override
+ public boolean contains(Object object) {
+ return colIndexOfValue(object) >= 0;
+ }
+ @Override
+ public boolean containsAll(Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+ @Override
+ public Iterator iterator() {
+ return new ArrayIterator(1);
+ }
+ @Override
+ public boolean remove(Object object) {
+ int index = colIndexOfValue(object);
+ if (index >= 0) {
+ colRemoveAt(index);
+ return true;
+ }
+ return false;
+ }
+ @Override
+ public boolean removeAll(Collection> collection) {
+ int N = colGetSize();
+ boolean changed = false;
+ for (int i=0; i collection) {
+ int N = colGetSize();
+ boolean changed = false;
+ for (int i=0; i T[] toArray(T[] array) {
+ return toArrayHelper(array, 1);
+ }
+ };
+ public static boolean containsAllHelper(Map map, Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!map.containsKey(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+ public static boolean removeAllHelper(Map map, Collection> collection) {
+ int oldSize = map.size();
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ map.remove(it.next());
+ }
+ return oldSize != map.size();
+ }
+ public static boolean retainAllHelper(Map map, Collection> collection) {
+ int oldSize = map.size();
+ Iterator it = map.keySet().iterator();
+ while (it.hasNext()) {
+ if (!collection.contains(it.next())) {
+ it.remove();
+ }
+ }
+ return oldSize != map.size();
+ }
+ public Object[] toArrayHelper(int offset) {
+ final int N = colGetSize();
+ Object[] result = new Object[N];
+ for (int i=0; i T[] toArrayHelper(T[] array, int offset) {
+ final int N = colGetSize();
+ if (array.length < N) {
+ @SuppressWarnings("unchecked") T[] newArray
+ = (T[]) Array.newInstance(array.getClass().getComponentType(), N);
+ array = newArray;
+ }
+ for (int i=0; i N) {
+ array[N] = null;
+ }
+ return array;
+ }
+ public static boolean equalsSetHelper(Set set, Object object) {
+ if (set == object) {
+ return true;
+ }
+ if (object instanceof Set) {
+ Set> s = (Set>) object;
+ try {
+ return set.size() == s.size() && set.containsAll(s);
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ }
+ return false;
+ }
+ public Set> getEntrySet() {
+ if (mEntrySet == null) {
+ mEntrySet = new EntrySet();
+ }
+ return mEntrySet;
+ }
+ public Set getKeySet() {
+ if (mKeySet == null) {
+ mKeySet = new KeySet();
+ }
+ return mKeySet;
+ }
+ public Collection getValues() {
+ if (mValues == null) {
+ mValues = new ValuesCollection();
+ }
+ return mValues;
+ }
+ protected abstract int colGetSize();
+ protected abstract Object colGetEntry(int index, int offset);
+ protected abstract int colIndexOfKey(Object key);
+ protected abstract int colIndexOfValue(Object key);
+ protected abstract Map colGetMap();
+ protected abstract void colPut(K key, V value);
+ protected abstract V colSetValue(int index, V value);
+ protected abstract void colRemoveAt(int index);
+ protected abstract void colClear();
+}
\ No newline at end of file
diff --git a/Common/src/main/java/android/util/Size.java b/Common/src/main/java/android/util/Size.java
new file mode 100644
index 00000000..35b40059
--- /dev/null
+++ b/Common/src/main/java/android/util/Size.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 android.util;
+// import static com.android.internal.util.Preconditions.checkNotNull;
+/**
+ * Immutable class for describing width and height dimensions in pixels.
+ */
+public final class Size {
+ /**
+ * Create a new immutable Size instance.
+ *
+ * @param width The width of the size, in pixels
+ * @param height The height of the size, in pixels
+ */
+ public Size(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+ /**
+ * Get the width of the size (in pixels).
+ * @return width
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+ /**
+ * Get the height of the size (in pixels).
+ * @return height
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+ /**
+ * Check if this size is equal to another size.
+ *
+ * Two sizes are equal if and only if both their widths and heights are
+ * equal.
+ *
+ *
+ * A size object is never equal to any other type of object.
+ *
+ *
+ * @return {@code true} if the objects were equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Size) {
+ Size other = (Size) obj;
+ return mWidth == other.mWidth && mHeight == other.mHeight;
+ }
+ return false;
+ }
+ /**
+ * Return the size represented as a string with the format {@code "WxH"}
+ *
+ * @return string representation of the size
+ */
+ @Override
+ public String toString() {
+ return mWidth + "x" + mHeight;
+ }
+ private static NumberFormatException invalidSize(String s) {
+ throw new NumberFormatException("Invalid Size: \"" + s + "\"");
+ }
+ /**
+ * Parses the specified string as a size value.
+ *
+ * The ASCII characters {@code \}{@code u002a} ('*') and
+ * {@code \}{@code u0078} ('x') are recognized as separators between
+ * the width and height.
+ *
+ * For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}.
+ * However, the method also handles sizes expressed in the
+ * following forms:
+ *
+ * "width{@code x}height" or
+ * "width{@code *}height" {@code => new Size(width, height)},
+ * where width and height are string integers potentially
+ * containing a sign, such as "-10", "+7" or "5".
+ *
+ * {@code
+ * Size.parseSize("3*+6").equals(new Size(3, 6)) == true
+ * Size.parseSize("-3x-6").equals(new Size(-3, -6)) == true
+ * Size.parseSize("4 by 3") => throws NumberFormatException
+ * }
+ *
+ * @param string the string representation of a size value.
+ * @return the size value represented by {@code string}.
+ *
+ * @throws NumberFormatException if {@code string} cannot be parsed
+ * as a size value.
+ * @throws NullPointerException if {@code string} was {@code null}
+ */
+ public static Size parseSize(String string)
+ throws NumberFormatException {
+ // checkNotNull(string, "string must not be null");
+ int sep_ix = string.indexOf('*');
+ if (sep_ix < 0) {
+ sep_ix = string.indexOf('x');
+ }
+ if (sep_ix < 0) {
+ throw invalidSize(string);
+ }
+ try {
+ return new Size(Integer.parseInt(string.substring(0, sep_ix)),
+ Integer.parseInt(string.substring(sep_ix + 1)));
+ } catch (NumberFormatException e) {
+ throw invalidSize(string);
+ }
+ }
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
+ return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2)));
+ }
+ private final int mWidth;
+ private final int mHeight;
+}
\ No newline at end of file
diff --git a/Common/src/main/java/android/util/SparseIntArray.java b/Common/src/main/java/android/util/SparseIntArray.java
new file mode 100644
index 00000000..675efaae
--- /dev/null
+++ b/Common/src/main/java/android/util/SparseIntArray.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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 android.util;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+import libcore.util.EmptyArray;
+import java.util.Arrays;
+/**
+ * SparseIntArrays map integers to integers. Unlike a normal array of integers,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Integers, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.
+ *
+ * It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * keyAt(int)
with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of valueAt(int)
.
+ */
+public class SparseIntArray implements Cloneable {
+
+ private int[] mKeys;
+
+ private int[] mValues;
+
+ private int mSize;
+ /**
+ * Creates a new SparseIntArray containing no mappings.
+ */
+ public SparseIntArray() {
+ this(10);
+ }
+ /**
+ * Creates a new SparseIntArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public SparseIntArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.INT;
+ mValues = EmptyArray.INT;
+ } else {
+ mKeys = new int[initialCapacity];
+ mValues = new int[mKeys.length];
+ }
+ mSize = 0;
+ }
+ @Override
+ public SparseIntArray clone() {
+ SparseIntArray clone = null;
+ try {
+ clone = (SparseIntArray) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+ /**
+ * Gets the int mapped from the specified key, or 0
+ * if no such mapping has been made.
+ */
+ public int get(int key) {
+ return get(key, 0);
+ }
+ /**
+ * Gets the int mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public int get(int key, int valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+ if (i < 0) {
+ return valueIfKeyNotFound;
+ } else {
+ return mValues[i];
+ }
+ }
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+ if (i >= 0) {
+ removeAt(i);
+ }
+ }
+ /**
+ * Removes the mapping at the given index.
+ */
+ public void removeAt(int index) {
+ System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+ mSize--;
+ }
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, int value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+ /**
+ * Returns the number of key-value mappings that this SparseIntArray
+ * currently stores.
+ */
+ public int size() {
+ return mSize;
+ }
+ /**
+ * Given an index in the range 0...size()-1
, returns
+ * the key from the index
th key-value mapping that this
+ * SparseIntArray stores.
+ *
+ * The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., keyAt(0)
will return the
+ * smallest key and keyAt(size()-1)
will return the largest
+ * key.
+ *
+ * For indices outside of the range 0...size()-1
, the behavior is undefined for
+ * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {link android.os.Build.VERSION_CODES#Q} and later.
+ */
+ public int keyAt(int index) {
+ if (index >= mSize) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mKeys[index];
+ }
+ /**
+ * Given an index in the range 0...size()-1
, returns
+ * the value from the index
th key-value mapping that this
+ * SparseIntArray stores.
+ *
+ * The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * valueAt(0)
will return the value associated with the
+ * smallest key and valueAt(size()-1)
will return the value
+ * associated with the largest key.
+ *
+ * For indices outside of the range 0...size()-1
, the behavior is undefined for
+ * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {link android.os.Build.VERSION_CODES#Q} and later.
+ */
+ public int valueAt(int index) {
+ if (index >= mSize) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mValues[index];
+ }
+ /**
+ * Directly set the value at a particular index.
+ *
+ * For indices outside of the range 0...size()-1
, the behavior is undefined for
+ * apps targeting {link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {link android.os.Build.VERSION_CODES#Q} and later.
+ */
+ public void setValueAt(int index, int value) {
+ if (index >= mSize) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ mValues[index] = value;
+ }
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(int value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+ return -1;
+ }
+ /**
+ * Removes all key-value mappings from this SparseIntArray.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, int value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+ /**
+ * Provides a copy of keys.
+ *
+ * @hide
+ * */
+ public int[] copyKeys() {
+ if (size() == 0) {
+ return null;
+ }
+ return Arrays.copyOf(mKeys, size());
+ }
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ int value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/androidx/annotation/NonNull.java b/Common/src/main/java/androidx/annotation/NonNull.java
new file mode 100644
index 00000000..e8847afe
--- /dev/null
+++ b/Common/src/main/java/androidx/annotation/NonNull.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 androidx.annotation;
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+/**
+ * Denotes that a parameter, field or method return value can never be null.
+ *
+ * This is a marker annotation and it has no specific attributes.
+ */
+@Documented
+@Retention(CLASS)
+@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
+public @interface NonNull {
+}
\ No newline at end of file
diff --git a/Common/src/main/java/androidx/annotation/Nullable.java b/Common/src/main/java/androidx/annotation/Nullable.java
new file mode 100644
index 00000000..059eaf5d
--- /dev/null
+++ b/Common/src/main/java/androidx/annotation/Nullable.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 androidx.annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+/**
+ * Denotes that a parameter, field or method return value can be null.
+ *
+ * When decorating a method call parameter, this denotes that the parameter can
+ * legitimately be null and the method will gracefully deal with it. Typically
+ * used on optional parameters.
+ *
+ * When decorating a method, this denotes the method might legitimately return
+ * null.
+ *
+ * This is a marker annotation and it has no specific attributes.
+ *
+ * @paramDoc This value may be {@code null}.
+ * @returnDoc This value may be {@code null}.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, PARAMETER, FIELD})
+public @interface Nullable {
+}
\ No newline at end of file
diff --git a/Common/src/main/java/androidx/annotation/StringRes.java b/Common/src/main/java/androidx/annotation/StringRes.java
new file mode 100644
index 00000000..52c0fa00
--- /dev/null
+++ b/Common/src/main/java/androidx/annotation/StringRes.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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 androidx.annotation;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+/**
+ * Denotes that an integer parameter, field or method return value is expected
+ * to be a String resource reference (e.g. {@code android.R.string.ok}).
+ */
+@Documented
+@Retention(CLASS)
+@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
+public @interface StringRes {
+}
\ No newline at end of file
diff --git a/Common/src/main/java/com/android/internal/util/ArrayUtils.java b/Common/src/main/java/com/android/internal/util/ArrayUtils.java
new file mode 100644
index 00000000..2e30c597
--- /dev/null
+++ b/Common/src/main/java/com/android/internal/util/ArrayUtils.java
@@ -0,0 +1,834 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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 com.android.internal.util;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import android.util.ArraySet;
+
+import dalvik.system.VMRuntime;
+import libcore.util.EmptyArray;
+import java.io.File;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.IntFunction;
+/**
+ * Static utility methods for arrays that aren't already included in {@link java.util.Arrays}.
+ */
+public class ArrayUtils {
+ private static final int CACHE_SIZE = 73;
+ private static Object[] sCache = new Object[CACHE_SIZE];
+ public static final File[] EMPTY_FILE = new File[0];
+ private ArrayUtils() { /* cannot be instantiated */ }
+
+ public static byte[] newUnpaddedByteArray(int minLen) {
+ return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen);
+ }
+ public static char[] newUnpaddedCharArray(int minLen) {
+ return (char[]) VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen);
+ }
+ public static int[] newUnpaddedIntArray(int minLen) {
+ return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen);
+ }
+ public static boolean[] newUnpaddedBooleanArray(int minLen) {
+ return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen);
+ }
+ public static long[] newUnpaddedLongArray(int minLen) {
+ return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen);
+ }
+ public static float[] newUnpaddedFloatArray(int minLen) {
+ return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen);
+ }
+ public static Object[] newUnpaddedObjectArray(int minLen) {
+ return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen);
+ }
+ @SuppressWarnings("unchecked")
+ public static T[] newUnpaddedArray(Class clazz, int minLen) {
+ return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen);
+ }
+
+ /**
+ * Checks if the beginnings of two byte arrays are equal.
+ *
+ * @param array1 the first byte array
+ * @param array2 the second byte array
+ * @param length the number of bytes to check
+ * @return true if they're equal, false otherwise
+ */
+ public static boolean equals(byte[] array1, byte[] array2, int length) {
+ if (length < 0) {
+ throw new IllegalArgumentException();
+ }
+ if (array1 == array2) {
+ return true;
+ }
+ if (array1 == null || array2 == null || array1.length < length || array2.length < length) {
+ return false;
+ }
+ for (int i = 0; i < length; i++) {
+ if (array1[i] != array2[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ /**
+ * Returns an empty array of the specified type. The intent is that
+ * it will return the same empty array every time to avoid reallocation,
+ * although this is not guaranteed.
+ */
+ @SuppressWarnings("unchecked")
+ public static T[] emptyArray(Class kind) {
+ if (kind == Object.class) {
+ return (T[]) EmptyArray.OBJECT;
+ }
+ int bucket = (kind.hashCode() & 0x7FFFFFFF) % CACHE_SIZE;
+ Object cache = sCache[bucket];
+ if (cache == null || cache.getClass().getComponentType() != kind) {
+ cache = Array.newInstance(kind, 0);
+ sCache[bucket] = cache;
+ // Log.e("cache", "new empty " + kind.getName() + " at " + bucket);
+ }
+ return (T[]) cache;
+ }
+ /**
+ * Returns the same array or an empty one if it's null.
+ */
+ public static @NonNull T[] emptyIfNull(@Nullable T[] items, Class kind) {
+ return items != null ? items : emptyArray(kind);
+ }
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable Collection> array) {
+ return array == null || array.isEmpty();
+ }
+ /**
+ * Checks if given map is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable Map, ?> map) {
+ return map == null || map.isEmpty();
+ }
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable T[] array) {
+ return array == null || array.length == 0;
+ }
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable int[] array) {
+ return array == null || array.length == 0;
+ }
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable long[] array) {
+ return array == null || array.length == 0;
+ }
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable byte[] array) {
+ return array == null || array.length == 0;
+ }
+ /**
+ * Checks if given array is null or has zero elements.
+ */
+ public static boolean isEmpty(@Nullable boolean[] array) {
+ return array == null || array.length == 0;
+ }
+ /**
+ * Length of the given array or 0 if it's null.
+ */
+ public static int size(@Nullable Object[] array) {
+ return array == null ? 0 : array.length;
+ }
+ /**
+ * Length of the given collection or 0 if it's null.
+ */
+ public static int size(@Nullable Collection> collection) {
+ return collection == null ? 0 : collection.size();
+ }
+ /**
+ * Length of the given map or 0 if it's null.
+ */
+ public static int size(@Nullable Map, ?> map) {
+ return map == null ? 0 : map.size();
+ }
+ /**
+ * Checks that value is present as at least one of the elements of the array.
+ * @param array the array to check in
+ * @param value the value to check for
+ * @return true if the value is present in the array
+ */
+ public static boolean contains(@Nullable T[] array, T value) {
+ return indexOf(array, value) != -1;
+ }
+ /**
+ * Return first index of {@code value} in {@code array}, or {@code -1} if
+ * not found.
+ */
+ public static int indexOf(@Nullable T[] array, T value) {
+ if (array == null) return -1;
+ for (int i = 0; i < array.length; i++) {
+ if (Objects.equals(array[i], value)) return i;
+ }
+ return -1;
+ }
+ /**
+ * Test if all {@code check} items are contained in {@code array}.
+ */
+ public static boolean containsAll(@Nullable T[] array, T[] check) {
+ if (check == null) return true;
+ for (T checkItem : check) {
+ if (!contains(array, checkItem)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ /**
+ * Test if any {@code check} items are contained in {@code array}.
+ */
+ public static boolean containsAny(@Nullable T[] array, T[] check) {
+ if (check == null) return false;
+ for (T checkItem : check) {
+ if (contains(array, checkItem)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ public static boolean contains(@Nullable int[] array, int value) {
+ if (array == null) return false;
+ for (int element : array) {
+ if (element == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+ public static boolean contains(@Nullable long[] array, long value) {
+ if (array == null) return false;
+ for (long element : array) {
+ if (element == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+ public static boolean contains(@Nullable char[] array, char value) {
+ if (array == null) return false;
+ for (char element : array) {
+ if (element == value) {
+ return true;
+ }
+ }
+ return false;
+ }
+ /**
+ * Test if all {@code check} items are contained in {@code array}.
+ */
+ public static boolean containsAll(@Nullable char[] array, char[] check) {
+ if (check == null) return true;
+ for (char checkItem : check) {
+ if (!contains(array, checkItem)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ public static long total(@Nullable long[] array) {
+ long total = 0;
+ if (array != null) {
+ for (long value : array) {
+ total += value;
+ }
+ }
+ return total;
+ }
+ /**
+ * @deprecated use {@code IntArray} instead
+ */
+ @Deprecated
+ public static int[] convertToIntArray(List list) {
+ int[] array = new int[list.size()];
+ for (int i = 0; i < list.size(); i++) {
+ array[i] = list.get(i);
+ }
+ return array;
+ }
+ public static @Nullable long[] convertToLongArray(@Nullable int[] intArray) {
+ if (intArray == null) return null;
+ long[] array = new long[intArray.length];
+ for (int i = 0; i < intArray.length; i++) {
+ array[i] = (long) intArray[i];
+ }
+ return array;
+ }
+ /**
+ * Returns the concatenation of the given arrays. Only works for object arrays, not for
+ * primitive arrays. See {@link #concat(byte[]...)} for a variant that works on byte arrays.
+ *
+ * @param kind The class of the array elements
+ * @param arrays The arrays to concatenate. Null arrays are treated as empty.
+ * @param The class of the array elements (inferred from kind).
+ * @return A single array containing all the elements of the parameter arrays.
+ */
+ @SuppressWarnings("unchecked")
+ public static @NonNull T[] concat(Class kind, @Nullable T[]... arrays) {
+ if (arrays == null || arrays.length == 0) {
+ return createEmptyArray(kind);
+ }
+ int totalLength = 0;
+ for (T[] item : arrays) {
+ if (item == null) {
+ continue;
+ }
+ totalLength += item.length;
+ }
+ // Optimization for entirely empty arrays.
+ if (totalLength == 0) {
+ return createEmptyArray(kind);
+ }
+ final T[] all = (T[]) Array.newInstance(kind, totalLength);
+ int pos = 0;
+ for (T[] item : arrays) {
+ if (item == null || item.length == 0) {
+ continue;
+ }
+ System.arraycopy(item, 0, all, pos, item.length);
+ pos += item.length;
+ }
+ return all;
+ }
+ private static @NonNull T[] createEmptyArray(Class kind) {
+ if (kind == String.class) {
+ return (T[]) EmptyArray.STRING;
+ } else if (kind == Object.class) {
+ return (T[]) EmptyArray.OBJECT;
+ }
+ return (T[]) Array.newInstance(kind, 0);
+ }
+ /**
+ * Returns the concatenation of the given byte arrays. Null arrays are treated as empty.
+ */
+ public static @NonNull byte[] concat(@Nullable byte[]... arrays) {
+ if (arrays == null) {
+ return new byte[0];
+ }
+ int totalLength = 0;
+ for (byte[] a : arrays) {
+ if (a != null) {
+ totalLength += a.length;
+ }
+ }
+ final byte[] result = new byte[totalLength];
+ int pos = 0;
+ for (byte[] a : arrays) {
+ if (a != null) {
+ System.arraycopy(a, 0, result, pos, a.length);
+ pos += a.length;
+ }
+ }
+ return result;
+ }
+ /**
+ * Adds value to given array if not already present, providing set-like
+ * behavior.
+ */
+ @SuppressWarnings("unchecked")
+ public static @NonNull T[] appendElement(Class kind, @Nullable T[] array, T element) {
+ return appendElement(kind, array, element, false);
+ }
+ /**
+ * Adds value to given array.
+ */
+ @SuppressWarnings("unchecked")
+ public static @NonNull T[] appendElement(Class kind, @Nullable T[] array, T element,
+ boolean allowDuplicates) {
+ final T[] result;
+ final int end;
+ if (array != null) {
+ if (!allowDuplicates && contains(array, element)) return array;
+ end = array.length;
+ result = (T[])Array.newInstance(kind, end + 1);
+ System.arraycopy(array, 0, result, 0, end);
+ } else {
+ end = 0;
+ result = (T[])Array.newInstance(kind, 1);
+ }
+ result[end] = element;
+ return result;
+ }
+ /**
+ * Removes value from given array if present, providing set-like behavior.
+ */
+ @SuppressWarnings("unchecked")
+ public static @Nullable T[] removeElement(Class kind, @Nullable T[] array, T element) {
+ if (array != null) {
+ if (!contains(array, element)) return array;
+ final int length = array.length;
+ for (int i = 0; i < length; i++) {
+ if (Objects.equals(array[i], element)) {
+ if (length == 1) {
+ return null;
+ }
+ T[] result = (T[])Array.newInstance(kind, length - 1);
+ System.arraycopy(array, 0, result, 0, i);
+ System.arraycopy(array, i + 1, result, i, length - i - 1);
+ return result;
+ }
+ }
+ }
+ return array;
+ }
+ /**
+ * Adds value to given array.
+ */
+ public static @NonNull int[] appendInt(@Nullable int[] cur, int val,
+ boolean allowDuplicates) {
+ if (cur == null) {
+ return new int[] { val };
+ }
+ final int N = cur.length;
+ if (!allowDuplicates) {
+ for (int i = 0; i < N; i++) {
+ if (cur[i] == val) {
+ return cur;
+ }
+ }
+ }
+ int[] ret = new int[N + 1];
+ System.arraycopy(cur, 0, ret, 0, N);
+ ret[N] = val;
+ return ret;
+ }
+ /**
+ * Adds value to given array if not already present, providing set-like
+ * behavior.
+ */
+ public static @NonNull int[] appendInt(@Nullable int[] cur, int val) {
+ return appendInt(cur, val, false);
+ }
+ /**
+ * Removes value from given array if present, providing set-like behavior.
+ */
+ public static @Nullable int[] removeInt(@Nullable int[] cur, int val) {
+ if (cur == null) {
+ return null;
+ }
+ final int N = cur.length;
+ for (int i = 0; i < N; i++) {
+ if (cur[i] == val) {
+ int[] ret = new int[N - 1];
+ if (i > 0) {
+ System.arraycopy(cur, 0, ret, 0, i);
+ }
+ if (i < (N - 1)) {
+ System.arraycopy(cur, i + 1, ret, i, N - i - 1);
+ }
+ return ret;
+ }
+ }
+ return cur;
+ }
+ /**
+ * Removes value from given array if present, providing set-like behavior.
+ */
+ public static @Nullable String[] removeString(@Nullable String[] cur, String val) {
+ if (cur == null) {
+ return null;
+ }
+ final int N = cur.length;
+ for (int i = 0; i < N; i++) {
+ if (Objects.equals(cur[i], val)) {
+ String[] ret = new String[N - 1];
+ if (i > 0) {
+ System.arraycopy(cur, 0, ret, 0, i);
+ }
+ if (i < (N - 1)) {
+ System.arraycopy(cur, i + 1, ret, i, N - i - 1);
+ }
+ return ret;
+ }
+ }
+ return cur;
+ }
+ /**
+ * Adds value to given array if not already present, providing set-like
+ * behavior.
+ */
+ public static @NonNull long[] appendLong(@Nullable long[] cur, long val,
+ boolean allowDuplicates) {
+ if (cur == null) {
+ return new long[] { val };
+ }
+ final int N = cur.length;
+ if (!allowDuplicates) {
+ for (int i = 0; i < N; i++) {
+ if (cur[i] == val) {
+ return cur;
+ }
+ }
+ }
+ long[] ret = new long[N + 1];
+ System.arraycopy(cur, 0, ret, 0, N);
+ ret[N] = val;
+ return ret;
+ }
+ /**
+ * Adds value to given array if not already present, providing set-like
+ * behavior.
+ */
+ public static @NonNull long[] appendLong(@Nullable long[] cur, long val) {
+ return appendLong(cur, val, false);
+ }
+ /**
+ * Removes value from given array if present, providing set-like behavior.
+ */
+ public static @Nullable long[] removeLong(@Nullable long[] cur, long val) {
+ if (cur == null) {
+ return null;
+ }
+ final int N = cur.length;
+ for (int i = 0; i < N; i++) {
+ if (cur[i] == val) {
+ long[] ret = new long[N - 1];
+ if (i > 0) {
+ System.arraycopy(cur, 0, ret, 0, i);
+ }
+ if (i < (N - 1)) {
+ System.arraycopy(cur, i + 1, ret, i, N - i - 1);
+ }
+ return ret;
+ }
+ }
+ return cur;
+ }
+ public static @Nullable long[] cloneOrNull(@Nullable long[] array) {
+ return (array != null) ? array.clone() : null;
+ }
+ /**
+ * Clones an array or returns null if the array is null.
+ */
+ public static @Nullable T[] cloneOrNull(@Nullable T[] array) {
+ return (array != null) ? array.clone() : null;
+ }
+ public static @Nullable ArraySet cloneOrNull(@Nullable ArraySet array) {
+ return (array != null) ? new ArraySet(array) : null;
+ }
+ public static @NonNull ArraySet add(@Nullable ArraySet cur, T val) {
+ if (cur == null) {
+ cur = new ArraySet<>();
+ }
+ cur.add(val);
+ return cur;
+ }
+ /**
+ * Similar to {@link Set#addAll(Collection)}}, but with support for set values of {@code null}.
+ */
+ public static @NonNull ArraySet addAll(@Nullable ArraySet cur,
+ @Nullable Collection val) {
+ if (cur == null) {
+ cur = new ArraySet<>();
+ }
+ if (val != null) {
+ cur.addAll(val);
+ }
+ return cur;
+ }
+ public static @Nullable ArraySet remove(@Nullable ArraySet cur, T val) {
+ if (cur == null) {
+ return null;
+ }
+ cur.remove(val);
+ if (cur.isEmpty()) {
+ return null;
+ } else {
+ return cur;
+ }
+ }
+ public static @NonNull ArrayList add(@Nullable ArrayList cur, T val) {
+ if (cur == null) {
+ cur = new ArrayList<>();
+ }
+ cur.add(val);
+ return cur;
+ }
+ public static @NonNull ArrayList add(@Nullable ArrayList cur, int index, T val) {
+ if (cur == null) {
+ cur = new ArrayList<>();
+ }
+ cur.add(index, val);
+ return cur;
+ }
+ public static @Nullable ArrayList remove(@Nullable ArrayList cur, T val) {
+ if (cur == null) {
+ return null;
+ }
+ cur.remove(val);
+ if (cur.isEmpty()) {
+ return null;
+ } else {
+ return cur;
+ }
+ }
+ public static boolean contains(@Nullable Collection cur, T val) {
+ return (cur != null) ? cur.contains(val) : false;
+ }
+ public static @Nullable T[] trimToSize(@Nullable T[] array, int size) {
+ if (array == null || size == 0) {
+ return null;
+ } else if (array.length == size) {
+ return array;
+ } else {
+ return Arrays.copyOf(array, size);
+ }
+ }
+ /**
+ * Returns true if the two ArrayLists are equal with respect to the objects they contain.
+ * The objects must be in the same order and be reference equal (== not .equals()).
+ */
+ public static boolean referenceEquals(ArrayList a, ArrayList b) {
+ if (a == b) {
+ return true;
+ }
+ final int sizeA = a.size();
+ final int sizeB = b.size();
+ if (a == null || b == null || sizeA != sizeB) {
+ return false;
+ }
+ boolean diff = false;
+ for (int i = 0; i < sizeA && !diff; i++) {
+ diff |= a.get(i) != b.get(i);
+ }
+ return !diff;
+ }
+ /**
+ * Removes elements that match the predicate in an efficient way that alters the order of
+ * elements in the collection. This should only be used if order is not important.
+ * @param collection The ArrayList from which to remove elements.
+ * @param predicate The predicate that each element is tested against.
+ * @return the number of elements removed.
+ */
+ public static int unstableRemoveIf(@Nullable ArrayList collection,
+ @NonNull java.util.function.Predicate predicate) {
+ if (collection == null) {
+ return 0;
+ }
+ final int size = collection.size();
+ int leftIdx = 0;
+ int rightIdx = size - 1;
+ while (leftIdx <= rightIdx) {
+ // Find the next element to remove moving left to right.
+ while (leftIdx < size && !predicate.test(collection.get(leftIdx))) {
+ leftIdx++;
+ }
+ // Find the next element to keep moving right to left.
+ while (rightIdx > leftIdx && predicate.test(collection.get(rightIdx))) {
+ rightIdx--;
+ }
+ if (leftIdx >= rightIdx) {
+ // Done.
+ break;
+ }
+ Collections.swap(collection, leftIdx, rightIdx);
+ leftIdx++;
+ rightIdx--;
+ }
+ // leftIdx is now at the end.
+ for (int i = size - 1; i >= leftIdx; i--) {
+ collection.remove(i);
+ }
+ return size - leftIdx;
+ }
+ public static @NonNull int[] defeatNullable(@Nullable int[] val) {
+ return (val != null) ? val : EmptyArray.INT;
+ }
+ public static @NonNull String[] defeatNullable(@Nullable String[] val) {
+ return (val != null) ? val : EmptyArray.STRING;
+ }
+ public static @NonNull File[] defeatNullable(@Nullable File[] val) {
+ return (val != null) ? val : EMPTY_FILE;
+ }
+ /**
+ * Throws {@link ArrayIndexOutOfBoundsException} if the index is out of bounds.
+ *
+ * @param len length of the array. Must be non-negative
+ * @param index the index to check
+ * @throws ArrayIndexOutOfBoundsException if the {@code index} is out of bounds of the array
+ */
+ public static void checkBounds(int len, int index) {
+ if (index < 0 || len <= index) {
+ throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index);
+ }
+ }
+ /**
+ * Throws {@link ArrayIndexOutOfBoundsException} if the range is out of bounds.
+ * @param len length of the array. Must be non-negative
+ * @param offset start index of the range. Must be non-negative
+ * @param count length of the range. Must be non-negative
+ * @throws ArrayIndexOutOfBoundsException if the range from {@code offset} with length
+ * {@code count} is out of bounds of the array
+ */
+ public static void throwsIfOutOfBounds(int len, int offset, int count) {
+ if (len < 0) {
+ throw new ArrayIndexOutOfBoundsException("Negative length: " + len);
+ }
+ if ((offset | count) < 0 || offset > len - count) {
+ throw new ArrayIndexOutOfBoundsException(
+ "length=" + len + "; regionStart=" + offset + "; regionLength=" + count);
+ }
+ }
+ /**
+ * Returns an array with values from {@code val} minus {@code null} values
+ *
+ * @param arrayConstructor typically {@code T[]::new} e.g. {@code String[]::new}
+ */
+ public static T[] filterNotNull(T[] val, IntFunction arrayConstructor) {
+ int nullCount = 0;
+ int size = size(val);
+ for (int i = 0; i < size; i++) {
+ if (val[i] == null) {
+ nullCount++;
+ }
+ }
+ if (nullCount == 0) {
+ return val;
+ }
+ T[] result = arrayConstructor.apply(size - nullCount);
+ int outIdx = 0;
+ for (int i = 0; i < size; i++) {
+ if (val[i] != null) {
+ result[outIdx++] = val[i];
+ }
+ }
+ return result;
+ }
+ /**
+ * Returns an array containing elements from the given one that match the given predicate.
+ * The returned array may, in some cases, be the reference to the input array.
+ */
+ public static @Nullable T[] filter(@Nullable T[] items,
+ @NonNull IntFunction arrayConstructor,
+ @NonNull java.util.function.Predicate predicate) {
+ if (isEmpty(items)) {
+ return items;
+ }
+ int matchesCount = 0;
+ int size = size(items);
+ final boolean[] tests = new boolean[size];
+ for (int i = 0; i < size; i++) {
+ tests[i] = predicate.test(items[i]);
+ if (tests[i]) {
+ matchesCount++;
+ }
+ }
+ if (matchesCount == items.length) {
+ return items;
+ }
+ T[] result = arrayConstructor.apply(matchesCount);
+ if (matchesCount == 0) {
+ return result;
+ }
+ int outIdx = 0;
+ for (int i = 0; i < size; i++) {
+ if (tests[i]) {
+ result[outIdx++] = items[i];
+ }
+ }
+ return result;
+ }
+ public static boolean startsWith(byte[] cur, byte[] val) {
+ if (cur == null || val == null) return false;
+ if (cur.length < val.length) return false;
+ for (int i = 0; i < val.length; i++) {
+ if (cur[i] != val[i]) return false;
+ }
+ return true;
+ }
+ /**
+ * Returns the first element from the array for which
+ * condition {@code predicate} is true, or null if there is no such element
+ */
+ public static @Nullable T find(@Nullable T[] items,
+ @NonNull java.util.function.Predicate predicate) {
+ if (isEmpty(items)) return null;
+ for (final T item : items) {
+ if (predicate.test(item)) return item;
+ }
+ return null;
+ }
+ public static String deepToString(Object value) {
+ if (value != null && value.getClass().isArray()) {
+ if (value.getClass() == boolean[].class) {
+ return Arrays.toString((boolean[]) value);
+ } else if (value.getClass() == byte[].class) {
+ return Arrays.toString((byte[]) value);
+ } else if (value.getClass() == char[].class) {
+ return Arrays.toString((char[]) value);
+ } else if (value.getClass() == double[].class) {
+ return Arrays.toString((double[]) value);
+ } else if (value.getClass() == float[].class) {
+ return Arrays.toString((float[]) value);
+ } else if (value.getClass() == int[].class) {
+ return Arrays.toString((int[]) value);
+ } else if (value.getClass() == long[].class) {
+ return Arrays.toString((long[]) value);
+ } else if (value.getClass() == short[].class) {
+ return Arrays.toString((short[]) value);
+ } else {
+ return Arrays.deepToString((Object[]) value);
+ }
+ } else {
+ return String.valueOf(value);
+ }
+ }
+ /**
+ * Returns the {@code i}-th item in {@code items}, if it exists and {@code items} is not {@code
+ * null}, otherwise returns {@code null}.
+ */
+ @Nullable
+ public static T getOrNull(@Nullable T[] items, int i) {
+ return (items != null && items.length > i) ? items[i] : null;
+ }
+ public static @Nullable T firstOrNull(T[] items) {
+ return items.length > 0 ? items[0] : null;
+ }
+ /**
+ * Creates a {@link List} from an array. Different from {@link Arrays#asList(Object[])} as that
+ * will use the parameter as the backing array, meaning changes are not isolated.
+ */
+ public static List toList(T[] array) {
+ List list = new ArrayList<>(array.length);
+ //noinspection ManualArrayToCollectionCopy
+ for (T item : array) {
+ //noinspection UseBulkOperation
+ list.add(item);
+ }
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java b/Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java
new file mode 100644
index 00000000..9dc53800
--- /dev/null
+++ b/Common/src/main/java/com/android/internal/util/GrowingArrayUtils.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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 com.android.internal.util;
+
+/**
+ * A helper class that aims to provide comparable growth performance to ArrayList, but on primitive
+ * arrays. Common array operations are implemented for efficient use in dynamic containers.
+ *
+ * All methods in this class assume that the length of an array is equivalent to its capacity and
+ * NOT the number of elements in the array. The current size of the array is always passed in as a
+ * parameter.
+ *
+ * @hide
+ */
+public final class GrowingArrayUtils {
+ /**
+ * Appends an element to the end of the array, growing the array if there is no more room.
+ * @param array The array to which to append the element. This must NOT be null.
+ * @param currentSize The number of elements in the array. Must be less than or equal to
+ * array.length.
+ * @param element The element to append.
+ * @return the array to which the element was appended. This may be different than the given
+ * array.
+ */
+ public static T[] append(T[] array, int currentSize, T element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 > array.length) {
+ @SuppressWarnings("unchecked")
+ T[] newArray = ArrayUtils.newUnpaddedArray(
+ (Class) array.getClass().getComponentType(), growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+ /**
+ * Primitive int version of {@link #append(Object[], int, Object)}.
+ */
+ public static int[] append(int[] array, int currentSize, int element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 > array.length) {
+ int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+ /**
+ * Primitive long version of {@link #append(Object[], int, Object)}.
+ */
+ public static long[] append(long[] array, int currentSize, long element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 > array.length) {
+ long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+ /**
+ * Primitive boolean version of {@link #append(Object[], int, Object)}.
+ */
+ public static boolean[] append(boolean[] array, int currentSize, boolean element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 > array.length) {
+ boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+ /**
+ * Primitive float version of {@link #append(Object[], int, Object)}.
+ */
+ public static float[] append(float[] array, int currentSize, float element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 > array.length) {
+ float[] newArray = ArrayUtils.newUnpaddedFloatArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, currentSize);
+ array = newArray;
+ }
+ array[currentSize] = element;
+ return array;
+ }
+ /**
+ * Inserts an element into the array at the specified index, growing the array if there is no
+ * more room.
+ *
+ * @param array The array to which to append the element. Must NOT be null.
+ * @param currentSize The number of elements in the array. Must be less than or equal to
+ * array.length.
+ * @param element The element to insert.
+ * @return the array to which the element was appended. This may be different than the given
+ * array.
+ */
+ public static T[] insert(T[] array, int currentSize, int index, T element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+ @SuppressWarnings("unchecked")
+ T[] newArray = ArrayUtils.newUnpaddedArray((Class)array.getClass().getComponentType(),
+ growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+ /**
+ * Primitive int version of {@link #insert(Object[], int, int, Object)}.
+ */
+ public static int[] insert(int[] array, int currentSize, int index, int element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+ int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+ /**
+ * Primitive long version of {@link #insert(Object[], int, int, Object)}.
+ */
+ public static long[] insert(long[] array, int currentSize, int index, long element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+ long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+ /**
+ * Primitive boolean version of {@link #insert(Object[], int, int, Object)}.
+ */
+ public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) {
+ assert currentSize <= array.length;
+ if (currentSize + 1 <= array.length) {
+ System.arraycopy(array, index, array, index + 1, currentSize - index);
+ array[index] = element;
+ return array;
+ }
+ boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
+ System.arraycopy(array, 0, newArray, 0, index);
+ newArray[index] = element;
+ System.arraycopy(array, index, newArray, index + 1, array.length - index);
+ return newArray;
+ }
+ /**
+ * Given the current size of an array, returns an ideal size to which the array should grow.
+ * This is typically double the given size, but should not be relied upon to do so in the
+ * future.
+ */
+ public static int growSize(int currentSize) {
+ return currentSize <= 4 ? 8 : currentSize * 2;
+ }
+ // Uninstantiable
+ private GrowingArrayUtils() {}
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt
similarity index 100%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt
rename to Common/src/main/java/com/github/serivesmejia/eocvsim/util/LogExt.kt
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt
similarity index 95%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt
rename to Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt
index e0dbed6b..1a293fdf 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt
+++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt
@@ -1,144 +1,146 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.util.event
-
-import com.github.serivesmejia.eocvsim.util.loggerOf
-
-class EventHandler(val name: String) : Runnable {
-
- val logger by loggerOf("${name}-EventHandler")
-
- private val lock = Any()
- private val onceLock = Any()
-
- val listeners: Array
- get() {
- synchronized(lock) {
- return internalListeners.toTypedArray()
- }
- }
-
- val onceListeners: Array
- get() {
- synchronized(onceLock) {
- return internalOnceListeners.toTypedArray()
- }
- }
-
- var callRightAway = false
-
- private val internalListeners = ArrayList()
- private val internalOnceListeners = ArrayList()
-
- override fun run() {
- for(listener in listeners) {
- try {
- runListener(listener, false)
- } catch (ex: Exception) {
- if(ex is InterruptedException) {
- logger.warn("Rethrowing InterruptedException...")
- throw ex
- } else {
- logger.error("Error while running listener ${listener.javaClass.name}", ex)
- }
- }
- }
-
- val toRemoveOnceListeners = mutableListOf()
-
- //executing "doOnce" listeners
- for(listener in onceListeners) {
- try {
- runListener(listener, true)
- } catch (ex: Exception) {
- if(ex is InterruptedException) {
- logger.warn("Rethrowing InterruptedException...")
- throw ex
- } else {
- logger.error("Error while running \"once\" ${listener.javaClass.name}", ex)
- }
- }
-
- toRemoveOnceListeners.add(listener)
- }
-
- synchronized(onceLock) {
- for(listener in toRemoveOnceListeners) {
- internalOnceListeners.remove(listener)
- }
- }
- }
-
- fun doOnce(listener: EventListener) {
- if(callRightAway)
- runListener(listener, true)
- else synchronized(onceLock) {
- internalOnceListeners.add(listener)
- }
- }
-
- fun doOnce(runnable: Runnable) = doOnce { runnable.run() }
-
-
- fun doPersistent(listener: EventListener) {
- synchronized(lock) {
- internalListeners.add(listener)
- }
-
- if(callRightAway) runListener(listener, false)
- }
-
- fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() }
-
- fun removePersistentListener(listener: EventListener) {
- if(internalListeners.contains(listener)) {
- synchronized(lock) { internalListeners.remove(listener) }
- }
- }
-
- fun removeOnceListener(listener: EventListener) {
- if(internalOnceListeners.contains(listener)) {
- synchronized(onceLock) { internalOnceListeners.remove(listener) }
- }
- }
-
- fun removeAllListeners() {
- removeAllPersistentListeners()
- removeAllOnceListeners()
- }
-
- fun removeAllPersistentListeners() = synchronized(lock) {
- internalListeners.clear()
- }
-
- fun removeAllOnceListeners() = synchronized(onceLock) {
- internalOnceListeners.clear()
- }
-
- operator fun invoke(listener: EventListener) = doPersistent(listener)
-
- private fun runListener(listener: EventListener, isOnce: Boolean) =
- listener.run(EventListenerRemover(this, listener, isOnce))
-
-}
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.util.event
+
+import com.github.serivesmejia.eocvsim.util.loggerOf
+
+class EventHandler(val name: String) : Runnable {
+
+ val logger by loggerOf("${name}-EventHandler")
+
+ private val lock = Any()
+ private val onceLock = Any()
+
+ val listeners: Array
+ get() {
+ synchronized(lock) {
+ return internalListeners.toTypedArray()
+ }
+ }
+
+ val onceListeners: Array
+ get() {
+ synchronized(onceLock) {
+ return internalOnceListeners.toTypedArray()
+ }
+ }
+
+ var callRightAway = false
+
+ private val internalListeners = ArrayList()
+ private val internalOnceListeners = ArrayList()
+
+ override fun run() {
+ for(listener in listeners) {
+ try {
+ runListener(listener, false)
+ } catch (ex: Exception) {
+ if(ex is InterruptedException) {
+ logger.warn("Rethrowing InterruptedException...")
+ throw ex
+ } else {
+ logger.error("Error while running listener ${listener.javaClass.name}", ex)
+ }
+ }
+ }
+
+ val toRemoveOnceListeners = mutableListOf()
+
+ //executing "doOnce" listeners
+ for(listener in onceListeners) {
+ try {
+ runListener(listener, true)
+ } catch (ex: Exception) {
+ if(ex is InterruptedException) {
+ logger.warn("Rethrowing InterruptedException...")
+ throw ex
+ } else {
+ logger.error("Error while running \"once\" ${listener.javaClass.name}", ex)
+ }
+ }
+
+ toRemoveOnceListeners.add(listener)
+ }
+
+ synchronized(onceLock) {
+ for(listener in toRemoveOnceListeners) {
+ internalOnceListeners.remove(listener)
+ }
+ }
+ }
+
+ fun doOnce(listener: EventListener) {
+ if(callRightAway)
+ runListener(listener, true)
+ else synchronized(onceLock) {
+ internalOnceListeners.add(listener)
+ }
+ }
+
+ fun doOnce(runnable: Runnable) = doOnce { runnable.run() }
+
+
+ fun doPersistent(listener: EventListener): EventListenerRemover {
+ synchronized(lock) {
+ internalListeners.add(listener)
+ }
+
+ if(callRightAway) runListener(listener, false)
+
+ return EventListenerRemover(this, listener, false)
+ }
+
+ fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() }
+
+ fun removePersistentListener(listener: EventListener) {
+ if(internalListeners.contains(listener)) {
+ synchronized(lock) { internalListeners.remove(listener) }
+ }
+ }
+
+ fun removeOnceListener(listener: EventListener) {
+ if(internalOnceListeners.contains(listener)) {
+ synchronized(onceLock) { internalOnceListeners.remove(listener) }
+ }
+ }
+
+ fun removeAllListeners() {
+ removeAllPersistentListeners()
+ removeAllOnceListeners()
+ }
+
+ fun removeAllPersistentListeners() = synchronized(lock) {
+ internalListeners.clear()
+ }
+
+ fun removeAllOnceListeners() = synchronized(onceLock) {
+ internalOnceListeners.clear()
+ }
+
+ operator fun invoke(listener: EventListener) = doPersistent(listener)
+
+ private fun runListener(listener: EventListener, isOnce: Boolean) =
+ listener.run(EventListenerRemover(this, listener, isOnce))
+
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt
similarity index 92%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt
rename to Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt
index 532802f9..38544228 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt
+++ b/Common/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt
@@ -1,42 +1,49 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.util.event
-
-fun interface EventListener {
- fun run(remover: EventListenerRemover)
-}
-
-class EventListenerRemover(
- val handler: EventHandler,
- val listener: EventListener,
- val isOnceListener: Boolean
-) {
- fun removeThis() {
- if(isOnceListener)
- handler.removeOnceListener(listener)
- else
- handler.removePersistentListener(listener)
- }
-
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.util.event
+
+fun interface EventListener {
+ fun run(remover: EventListenerRemover)
+}
+
+class EventListenerRemover(
+ val handler: EventHandler,
+ val listener: EventListener,
+ val isOnceListener: Boolean
+) {
+
+ private val attached = mutableListOf()
+
+ fun removeThis() {
+ if(isOnceListener)
+ handler.removeOnceListener(listener)
+ else
+ handler.removePersistentListener(listener)
+ }
+
+ fun attach(remover: EventListenerRemover) {
+
+ }
+
}
\ No newline at end of file
diff --git a/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java
new file mode 100644
index 00000000..5b9b8435
--- /dev/null
+++ b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/Autonomous.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2015 Robert Atkinson
+ *
+ * Ported from the Swerve library by Craig MacFarlane
+ * Based upon contributions and original idea by dmssargent.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * (subject to the limitations in the disclaimer below) provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Robert Atkinson, Craig MacFarlane nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS
+ * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.qualcomm.robotcore.eventloop.opmode;
+
+import java.lang.annotation.*;
+
+/**
+ * Provides an easy and non-centralized way of determining the OpMode list
+ * shown on an FTC Driver Station. Put an {@link Autonomous} annotation on
+ * your autonomous OpModes that you want to show up in the driver station display.
+ *
+ * If you want to temporarily disable an opmode, then set then also add
+ * a {@link Disabled} annotation to it.
+ *
+ * @see TeleOp
+ * @see Disabled
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Autonomous
+{
+ /**
+ * The name to be used on the driver station display. If empty, the name of
+ * the OpMode class will be used.
+ * @return the name to use for the OpMode in the driver station.
+ */
+ String name() default "";
+
+ /**
+ * Optionally indicates a group of other OpModes with which the annotated
+ * OpMode should be sorted on the driver station OpMode list.
+ * @return the group into which the annotated OpMode is to be categorized
+ */
+ String group() default "";
+
+ /**
+ * The name of the TeleOp OpMode you'd like to have automagically preselected
+ * on the Driver Station when selecting this Autonomous OpMode. If empty, then
+ * nothing will be automagically preselected.
+ *
+ * @return see above
+ */
+ String preselectTeleOp() default "";
+}
\ No newline at end of file
diff --git a/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java
new file mode 100644
index 00000000..cc6ac9f8
--- /dev/null
+++ b/Common/src/main/java/com/qualcomm/robotcore/eventloop/opmode/TeleOp.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2015 Robert Atkinson
+ *
+ * Ported from the Swerve library by Craig MacFarlane
+ * Based upon contributions and original idea by dmssargent.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * (subject to the limitations in the disclaimer below) provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Robert Atkinson, Craig MacFarlane nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS
+ * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.qualcomm.robotcore.eventloop.opmode;
+
+import java.lang.annotation.*;
+
+/**
+ * Provides an easy and non-centralized way of determining the OpMode list
+ * shown on an FTC Driver Station. Put an {@link TeleOp} annotation on
+ * your teleop OpModes that you want to show up in the driver station display.
+ *
+ * If you want to temporarily disable an opmode from showing up, then set then also add
+ * a {@link Disabled} annotation to it.
+ *
+ * @see Autonomous
+ * @see Disabled
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TeleOp
+{
+ /**
+ * The name to be used on the driver station display. If empty, the name of
+ * the OpMode class will be used.
+ * @return the name to use for the OpMode on the driver station
+ */
+ String name() default "";
+
+ /**
+ * Optionally indicates a group of other OpModes with which the annotated
+ * OpMode should be sorted on the driver station OpMode list.
+ * @return the group into which the annotated OpMode is to be categorized
+ */
+ String group() default "";
+}
\ No newline at end of file
diff --git a/Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java b/Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java
new file mode 100644
index 00000000..cfbe3c70
--- /dev/null
+++ b/Common/src/main/java/com/qualcomm/robotcore/exception/RobotCoreException.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2014, 2015 Qualcomm Technologies Inc
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted
+ * (subject to the limitations in the disclaimer below) provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Qualcomm Technologies Inc nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS
+ * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.qualcomm.robotcore.exception;
+
+/*
+ * RobotCoreException
+ *
+ * An exception used commonly by the RobotCore library
+ */
+public class RobotCoreException extends Exception {
+
+ public RobotCoreException(String message) {
+ super(message);
+ }
+ public RobotCoreException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public RobotCoreException(String format, Object... args) {
+ super(String.format(format, args));
+ }
+
+ public static RobotCoreException createChained(Exception e, String format, Object... args) {
+ return new RobotCoreException(String.format(format, args), e);
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java b/Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java
new file mode 100644
index 00000000..2f5b78c0
--- /dev/null
+++ b/Common/src/main/java/com/qualcomm/robotcore/util/RobotLog.java
@@ -0,0 +1,17 @@
+package com.qualcomm.robotcore.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RobotLog {
+
+ private RobotLog() { }
+
+ public static void ee(String tag, Throwable throwable, String message) {
+ LoggerFactory.getLogger(tag).error(message, throwable);
+ }
+
+ public static void ee(String tag, String message) {
+ LoggerFactory.getLogger(tag).error(message);
+ }
+}
diff --git a/Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java b/Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java
new file mode 100644
index 00000000..0456d28d
--- /dev/null
+++ b/Common/src/main/java/com/qualcomm/robotcore/util/SerialNumber.java
@@ -0,0 +1,106 @@
+/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Qualcomm Technologies Inc nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+package com.qualcomm.robotcore.util;
+
+public class SerialNumber {
+
+
+ protected final String serialNumberString;
+
+ //------------------------------------------------------------------------------------------------
+ // Construction
+ //------------------------------------------------------------------------------------------------
+
+ /**
+ * Constructs a serial number using the supplied initialization string. If the initialization
+ * string is a legacy form of fake serial number, a unique fake serial number is created.
+ *
+ * @param serialNumberString the initialization string for the serial number.
+ */
+ protected SerialNumber(String serialNumberString) {
+ this.serialNumberString = serialNumberString;
+ }
+
+
+ /**
+ * Returns the string contents of the serial number. Result is not intended to be
+ * displayed to humans.
+ * @see #toString()
+ */
+ public String getString() {
+ return serialNumberString;
+ }
+
+
+ /**
+ * Returns the {@link SerialNumber} of the device associated with this one that would appear
+ * in a {link ScannedDevices}.
+ */
+ public SerialNumber getScannableDeviceSerialNumber() {
+ return this;
+ }
+
+ //------------------------------------------------------------------------------------------------
+ // Comparison
+ //------------------------------------------------------------------------------------------------
+
+ public boolean matches(Object pattern) {
+ return this.equals(pattern);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object == null) return false;
+ if (object == this) return true;
+
+ if (object instanceof SerialNumber) {
+ return serialNumberString.equals(((SerialNumber) object).serialNumberString);
+ }
+
+ if (object instanceof String) {
+ return this.equals((String)object);
+ }
+
+ return false;
+ }
+
+ // separate method to avoid annoying Android Studio inspection warnings when comparing SerialNumber against String
+ public boolean equals(String string) {
+ return serialNumberString.equals(string);
+ }
+
+ @Override
+ public int hashCode() {
+ return serialNumberString.hashCode() ^ 0xabcd9873;
+ }
+
+}
diff --git a/Common/src/main/java/dalvik/system/VMRuntime.java b/Common/src/main/java/dalvik/system/VMRuntime.java
new file mode 100644
index 00000000..a8b450cc
--- /dev/null
+++ b/Common/src/main/java/dalvik/system/VMRuntime.java
@@ -0,0 +1,22 @@
+package dalvik.system;
+
+import java.lang.reflect.Array;
+
+public class VMRuntime {
+
+ // singleton class
+
+ private static VMRuntime runtime = new VMRuntime();
+
+ private VMRuntime() {
+ }
+
+ public static VMRuntime getRuntime() {
+ return runtime;
+ }
+
+ public Object newUnpaddedArray(Class> componentType, int length) {
+ return Array.newInstance(componentType, length); // we do a little bit of trolling -SEM
+ }
+
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java b/Common/src/main/java/io/github/deltacv/common/image/BufferedImageRecycler.java
similarity index 96%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java
rename to Common/src/main/java/io/github/deltacv/common/image/BufferedImageRecycler.java
index d2b21b3f..2da834a7 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java
+++ b/Common/src/main/java/io/github/deltacv/common/image/BufferedImageRecycler.java
@@ -1,107 +1,107 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.util.image;
-
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.util.concurrent.ArrayBlockingQueue;
-
-public class BufferedImageRecycler {
-
- private final RecyclableBufferedImage[] allBufferedImages;
- private final ArrayBlockingQueue availableBufferedImages;
-
- public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight, int allImgType) {
- allBufferedImages = new RecyclableBufferedImage[num];
- availableBufferedImages = new ArrayBlockingQueue<>(num);
-
- for (int i = 0; i < allBufferedImages.length; i++) {
- allBufferedImages[i] = new RecyclableBufferedImage(i, allImgWidth, allImgHeight, allImgType);
- availableBufferedImages.add(allBufferedImages[i]);
- }
- }
-
- public BufferedImageRecycler(int num, Dimension allImgSize, int allImgType) {
- this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), allImgType);
- }
-
- public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight) {
- this(num, allImgWidth, allImgHeight, BufferedImage.TYPE_3BYTE_BGR);
- }
-
- public BufferedImageRecycler(int num, Dimension allImgSize) {
- this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
- }
-
- public boolean isOnUse() { return allBufferedImages.length != availableBufferedImages.size(); }
-
- public synchronized RecyclableBufferedImage takeBufferedImage() {
-
- if (availableBufferedImages.size() == 0) {
- throw new RuntimeException("All buffered images have been checked out!");
- }
-
- RecyclableBufferedImage buffImg = null;
- try {
- buffImg = availableBufferedImages.take();
- buffImg.checkedOut = true;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- return buffImg;
-
- }
-
- public synchronized void returnBufferedImage(RecyclableBufferedImage buffImg) {
- if (buffImg != allBufferedImages[buffImg.idx]) {
- throw new IllegalArgumentException("This BufferedImage does not belong to this recycler!");
- }
-
- if (buffImg.checkedOut) {
- buffImg.checkedOut = false;
- buffImg.flush();
- availableBufferedImages.add(buffImg);
- } else {
- throw new IllegalArgumentException("This BufferedImage has already been returned!");
- }
- }
-
- public synchronized void flushAll() {
- for(BufferedImage img : allBufferedImages) {
- img.flush();
- }
- }
-
- public static class RecyclableBufferedImage extends BufferedImage {
- private int idx = -1;
- private volatile boolean checkedOut = false;
-
- private RecyclableBufferedImage(int idx, int width, int height, int imageType) {
- super(width, height, imageType);
- this.idx = idx;
- }
- }
-
-}
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.common.image;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.concurrent.ArrayBlockingQueue;
+
+public class BufferedImageRecycler {
+
+ private final RecyclableBufferedImage[] allBufferedImages;
+ private final ArrayBlockingQueue availableBufferedImages;
+
+ public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight, int allImgType) {
+ allBufferedImages = new RecyclableBufferedImage[num];
+ availableBufferedImages = new ArrayBlockingQueue<>(num);
+
+ for (int i = 0; i < allBufferedImages.length; i++) {
+ allBufferedImages[i] = new RecyclableBufferedImage(i, allImgWidth, allImgHeight, allImgType);
+ availableBufferedImages.add(allBufferedImages[i]);
+ }
+ }
+
+ public BufferedImageRecycler(int num, Dimension allImgSize, int allImgType) {
+ this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), allImgType);
+ }
+
+ public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight) {
+ this(num, allImgWidth, allImgHeight, BufferedImage.TYPE_3BYTE_BGR);
+ }
+
+ public BufferedImageRecycler(int num, Dimension allImgSize) {
+ this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+ }
+
+ public boolean isOnUse() { return allBufferedImages.length != availableBufferedImages.size(); }
+
+ public synchronized RecyclableBufferedImage takeBufferedImage() {
+
+ if (availableBufferedImages.size() == 0) {
+ throw new RuntimeException("All buffered images have been checked out!");
+ }
+
+ RecyclableBufferedImage buffImg = null;
+ try {
+ buffImg = availableBufferedImages.take();
+ buffImg.checkedOut = true;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return buffImg;
+
+ }
+
+ public synchronized void returnBufferedImage(RecyclableBufferedImage buffImg) {
+ if (buffImg != allBufferedImages[buffImg.idx]) {
+ throw new IllegalArgumentException("This BufferedImage does not belong to this recycler!");
+ }
+
+ if (buffImg.checkedOut) {
+ buffImg.checkedOut = false;
+ buffImg.flush();
+ availableBufferedImages.add(buffImg);
+ } else {
+ throw new IllegalArgumentException("This BufferedImage has already been returned!");
+ }
+ }
+
+ public synchronized void flushAll() {
+ for(BufferedImage img : allBufferedImages) {
+ img.flush();
+ }
+ }
+
+ public static class RecyclableBufferedImage extends BufferedImage {
+ private int idx = -1;
+ private volatile boolean checkedOut = false;
+
+ private RecyclableBufferedImage(int idx, int width, int height, int imageType) {
+ super(width, height, imageType);
+ this.idx = idx;
+ }
+ }
+
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java b/Common/src/main/java/io/github/deltacv/common/image/DynamicBufferedImageRecycler.java
similarity index 93%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java
rename to Common/src/main/java/io/github/deltacv/common/image/DynamicBufferedImageRecycler.java
index b4b50f3f..50e5cd41 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java
+++ b/Common/src/main/java/io/github/deltacv/common/image/DynamicBufferedImageRecycler.java
@@ -1,78 +1,76 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.util.image;
-
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-public class DynamicBufferedImageRecycler {
-
- private final HashMap recyclers = new HashMap<>();
-
- public synchronized BufferedImage giveBufferedImage(Dimension size, int recyclerSize) {
-
- //look for existing buff image recycler with desired dimensions
- for(Map.Entry entry : recyclers.entrySet()) {
- Dimension dimension = entry.getKey();
- BufferedImageRecycler recycler = entry.getValue();
-
- if(dimension.equals(size)) {
- BufferedImage buffImg = recycler.takeBufferedImage();
- buffImg.flush();
- return buffImg;
- } else if(!recycler.isOnUse()) {
- recycler.flushAll();
- recyclers.remove(dimension);
- }
- }
-
- //create new one if didn't found an existing recycler
- BufferedImageRecycler recycler = new BufferedImageRecycler(recyclerSize, size);
- recyclers.put(size, recycler);
-
- BufferedImage buffImg = recycler.takeBufferedImage();
-
- return buffImg;
- }
-
- public synchronized void returnBufferedImage(BufferedImage buffImg) {
- Dimension dimension = new Dimension(buffImg.getWidth(), buffImg.getHeight());
-
- BufferedImageRecycler recycler = recyclers.get(dimension);
-
- if(recycler != null)
- recycler.returnBufferedImage((BufferedImageRecycler.RecyclableBufferedImage) buffImg);
- }
-
- public synchronized void flushAll() {
- for(BufferedImageRecycler recycler : recyclers.values()) {
- recycler.flushAll();
- }
- }
-
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.common.image;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.HashMap;
+import java.util.Map;
+
+public class DynamicBufferedImageRecycler {
+
+ private final HashMap recyclers = new HashMap<>();
+
+ public synchronized BufferedImage giveBufferedImage(Dimension size, int recyclerSize) {
+
+ //look for existing buff image recycler with desired dimensions
+ for(Map.Entry entry : recyclers.entrySet()) {
+ Dimension dimension = entry.getKey();
+ BufferedImageRecycler recycler = entry.getValue();
+
+ if(dimension.equals(size)) {
+ BufferedImage buffImg = recycler.takeBufferedImage();
+ buffImg.flush();
+ return buffImg;
+ } else if(!recycler.isOnUse()) {
+ recycler.flushAll();
+ recyclers.remove(dimension);
+ }
+ }
+
+ //create new one if didn't found an existing recycler
+ BufferedImageRecycler recycler = new BufferedImageRecycler(recyclerSize, size);
+ recyclers.put(size, recycler);
+
+ BufferedImage buffImg = recycler.takeBufferedImage();
+
+ return buffImg;
+ }
+
+ public synchronized void returnBufferedImage(BufferedImage buffImg) {
+ Dimension dimension = new Dimension(buffImg.getWidth(), buffImg.getHeight());
+
+ BufferedImageRecycler recycler = recyclers.get(dimension);
+
+ if(recycler != null)
+ recycler.returnBufferedImage((BufferedImageRecycler.RecyclableBufferedImage) buffImg);
+ }
+
+ public synchronized void flushAll() {
+ for(BufferedImageRecycler recycler : recyclers.values()) {
+ recycler.flushAll();
+ }
+ }
+
}
\ No newline at end of file
diff --git a/Common/src/main/java/io/github/deltacv/common/image/MatPoster.java b/Common/src/main/java/io/github/deltacv/common/image/MatPoster.java
new file mode 100644
index 00000000..dd7357b7
--- /dev/null
+++ b/Common/src/main/java/io/github/deltacv/common/image/MatPoster.java
@@ -0,0 +1,13 @@
+package io.github.deltacv.common.image;
+
+import org.opencv.core.Mat;
+
+public interface MatPoster {
+
+ default void post(Mat m) {
+ post(m, null);
+ }
+
+ void post(Mat m, Object context);
+
+}
diff --git a/Common/src/main/java/io/github/deltacv/common/pipeline/util/PipelineStatisticsCalculator.kt b/Common/src/main/java/io/github/deltacv/common/pipeline/util/PipelineStatisticsCalculator.kt
new file mode 100644
index 00000000..b59d5d7d
--- /dev/null
+++ b/Common/src/main/java/io/github/deltacv/common/pipeline/util/PipelineStatisticsCalculator.kt
@@ -0,0 +1,61 @@
+package io.github.deltacv.common.pipeline.util
+
+import com.qualcomm.robotcore.util.ElapsedTime
+import com.qualcomm.robotcore.util.MovingStatistics
+import kotlin.math.roundToInt
+
+class PipelineStatisticsCalculator {
+
+ private lateinit var msFrameIntervalRollingAverage: MovingStatistics
+ private lateinit var msUserPipelineRollingAverage: MovingStatistics
+ private lateinit var msTotalFrameProcessingTimeRollingAverage: MovingStatistics
+ private lateinit var timer: ElapsedTime
+
+ private var currentFrameStartTime = 0L
+ private var pipelineStart = 0L
+
+ var avgFps = 0f
+ private set
+ var avgPipelineTime = 0
+ private set
+ var avgOverheadTime = 0
+ private set
+ var avgTotalFrameTime = 0
+ private set
+
+ fun init() {
+ msFrameIntervalRollingAverage = MovingStatistics(30)
+ msUserPipelineRollingAverage = MovingStatistics(30)
+ msTotalFrameProcessingTimeRollingAverage = MovingStatistics(30)
+ timer = ElapsedTime()
+ }
+
+ fun newInputFrameStart() {
+ currentFrameStartTime = System.currentTimeMillis();
+ }
+
+ fun newPipelineFrameStart() {
+ msFrameIntervalRollingAverage.add(timer.milliseconds())
+ timer.reset()
+
+ val secondsPerFrame = msFrameIntervalRollingAverage.mean / 1000.0
+ avgFps = (1.0 / secondsPerFrame).toFloat()
+ }
+
+ fun beforeProcessFrame() {
+ pipelineStart = System.currentTimeMillis()
+ }
+
+ fun afterProcessFrame() {
+ msUserPipelineRollingAverage.add((System.currentTimeMillis() - pipelineStart).toDouble())
+ avgPipelineTime = msUserPipelineRollingAverage.mean.roundToInt()
+ }
+
+ fun endFrame() {
+ msTotalFrameProcessingTimeRollingAverage.add((System.currentTimeMillis() - currentFrameStartTime).toDouble())
+
+ avgTotalFrameTime = msTotalFrameProcessingTimeRollingAverage.mean.roundToInt()
+ avgOverheadTime = avgTotalFrameTime - avgPipelineTime
+ }
+
+}
\ No newline at end of file
diff --git a/Common/src/main/java/libcore/util/EmptyArray.java b/Common/src/main/java/libcore/util/EmptyArray.java
new file mode 100644
index 00000000..c1184d27
--- /dev/null
+++ b/Common/src/main/java/libcore/util/EmptyArray.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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 libcore.util;
+import android.annotation.NonNull;
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Annotation;
+/**
+ * Empty array is immutable. Use a shared empty array to avoid allocation.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class EmptyArray {
+ private EmptyArray() {}
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull boolean[] BOOLEAN = new boolean[0];
+ /** @hide */
+
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull byte[] BYTE = new byte[0];
+ /** @hide */
+ public static final char[] CHAR = new char[0];
+ /** @hide */
+ public static final double[] DOUBLE = new double[0];
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull float[] FLOAT = new float[0];
+ /** @hide */
+
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull int[] INT = new int[0];
+ /** @hide */
+
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull long[] LONG = new long[0];
+ /** @hide */
+ public static final Class>[] CLASS = new Class[0];
+ /** @hide */
+
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull Object[] OBJECT = new Object[0];
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull String[] STRING = new String[0];
+ /** @hide */
+ public static final Throwable[] THROWABLE = new Throwable[0];
+ /** @hide */
+ public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
+ /** @hide */
+ public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0];
+ /** @hide */
+ public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE =
+ new java.lang.reflect.TypeVariable[0];
+ /** @hide */
+ public static final Annotation[] ANNOTATION = new Annotation[0];
+}
\ No newline at end of file
diff --git a/Common/src/main/java/libcore/util/FP16.java b/Common/src/main/java/libcore/util/FP16.java
new file mode 100644
index 00000000..e6864370
--- /dev/null
+++ b/Common/src/main/java/libcore/util/FP16.java
@@ -0,0 +1,794 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 libcore.util;
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import android.annotation.SystemApi;
+/**
+ * The {@code FP16} class is a wrapper and a utility class to manipulate half-precision 16-bit
+ * IEEE 754
+ * floating point data types (also called fp16 or binary16). A half-precision float can be
+ * created from or converted to single-precision floats, and is stored in a short data type.
+ *
+ *
The IEEE 754 standard specifies an fp16 as having the following format:
+ *
+ * - Sign bit: 1 bit
+ * - Exponent width: 5 bits
+ * - Significand: 10 bits
+ *
+ *
+ * The format is laid out as follows:
+ *
+ * 1 11111 1111111111
+ * ^ --^-- -----^----
+ * sign | |_______ significand
+ * |
+ * -- exponent
+ *
+ *
+ * Half-precision floating points can be useful to save memory and/or
+ * bandwidth at the expense of range and precision when compared to single-precision
+ * floating points (fp32).
+ * To help you decide whether fp16 is the right storage type for you need, please
+ * refer to the table below that shows the available precision throughout the range of
+ * possible values. The precision column indicates the step size between two
+ * consecutive numbers in a specific part of the range.
+ *
+ *
+ * Range start | Precision |
+ * 0 | 1 ⁄ 16,777,216 |
+ * 1 ⁄ 16,384 | 1 ⁄ 16,777,216 |
+ * 1 ⁄ 8,192 | 1 ⁄ 8,388,608 |
+ * 1 ⁄ 4,096 | 1 ⁄ 4,194,304 |
+ * 1 ⁄ 2,048 | 1 ⁄ 2,097,152 |
+ * 1 ⁄ 1,024 | 1 ⁄ 1,048,576 |
+ * 1 ⁄ 512 | 1 ⁄ 524,288 |
+ * 1 ⁄ 256 | 1 ⁄ 262,144 |
+ * 1 ⁄ 128 | 1 ⁄ 131,072 |
+ * 1 ⁄ 64 | 1 ⁄ 65,536 |
+ * 1 ⁄ 32 | 1 ⁄ 32,768 |
+ * 1 ⁄ 16 | 1 ⁄ 16,384 |
+ * 1 ⁄ 8 | 1 ⁄ 8,192 |
+ * 1 ⁄ 4 | 1 ⁄ 4,096 |
+ * 1 ⁄ 2 | 1 ⁄ 2,048 |
+ * 1 | 1 ⁄ 1,024 |
+ * 2 | 1 ⁄ 512 |
+ * 4 | 1 ⁄ 256 |
+ * 8 | 1 ⁄ 128 |
+ * 16 | 1 ⁄ 64 |
+ * 32 | 1 ⁄ 32 |
+ * 64 | 1 ⁄ 16 |
+ * 128 | 1 ⁄ 8 |
+ * 256 | 1 ⁄ 4 |
+ * 512 | 1 ⁄ 2 |
+ * 1,024 | 1 |
+ * 2,048 | 2 |
+ * 4,096 | 4 |
+ * 8,192 | 8 |
+ * 16,384 | 16 |
+ * 32,768 | 32 |
+ *
+ *
+ * This table shows that numbers higher than 1024 lose all fractional precision.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class FP16 {
+ /**
+ * The number of bits used to represent a half-precision float value.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int SIZE = 16;
+ /**
+ * Epsilon is the difference between 1.0 and the next value representable
+ * by a half-precision floating-point.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short EPSILON = (short) 0x1400;
+ /**
+ * Maximum exponent a finite half-precision float may have.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int MAX_EXPONENT = 15;
+ /**
+ * Minimum exponent a normalized half-precision float may have.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int MIN_EXPONENT = -14;
+ /**
+ * Smallest negative value a half-precision float may have.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short LOWEST_VALUE = (short) 0xfbff;
+ /**
+ * Maximum positive finite value a half-precision float may have.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short MAX_VALUE = (short) 0x7bff;
+ /**
+ * Smallest positive normal value a half-precision float may have.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short MIN_NORMAL = (short) 0x0400;
+ /**
+ * Smallest positive non-zero value a half-precision float may have.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short MIN_VALUE = (short) 0x0001;
+ /**
+ * A Not-a-Number representation of a half-precision float.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short NaN = (short) 0x7e00;
+ /**
+ * Negative infinity of type half-precision float.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short NEGATIVE_INFINITY = (short) 0xfc00;
+ /**
+ * Negative 0 of type half-precision float.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short NEGATIVE_ZERO = (short) 0x8000;
+ /**
+ * Positive infinity of type half-precision float.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short POSITIVE_INFINITY = (short) 0x7c00;
+ /**
+ * Positive 0 of type half-precision float.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final short POSITIVE_ZERO = (short) 0x0000;
+ /**
+ * The offset to shift by to obtain the sign bit.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int SIGN_SHIFT = 15;
+ /**
+ * The offset to shift by to obtain the exponent bits.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int EXPONENT_SHIFT = 10;
+ /**
+ * The bitmask to AND a number with to obtain the sign bit.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int SIGN_MASK = 0x8000;
+ /**
+ * The bitmask to AND a number shifted by {@link #EXPONENT_SHIFT} right, to obtain exponent bits.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int SHIFTED_EXPONENT_MASK = 0x1f;
+ /**
+ * The bitmask to AND a number with to obtain significand bits.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int SIGNIFICAND_MASK = 0x3ff;
+ /**
+ * The bitmask to AND with to obtain exponent and significand bits.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int EXPONENT_SIGNIFICAND_MASK = 0x7fff;
+ /**
+ * The offset of the exponent from the actual value.
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int EXPONENT_BIAS = 15;
+ private static final int FP32_SIGN_SHIFT = 31;
+ private static final int FP32_EXPONENT_SHIFT = 23;
+ private static final int FP32_SHIFTED_EXPONENT_MASK = 0xff;
+ private static final int FP32_SIGNIFICAND_MASK = 0x7fffff;
+ private static final int FP32_EXPONENT_BIAS = 127;
+ private static final int FP32_QNAN_MASK = 0x400000;
+ private static final int FP32_DENORMAL_MAGIC = 126 << 23;
+ private static final float FP32_DENORMAL_FLOAT = Float.intBitsToFloat(FP32_DENORMAL_MAGIC);
+ /** Hidden constructor to prevent instantiation. */
+ private FP16() {}
+ /**
+ * Compares the two specified half-precision float values. The following
+ * conditions apply during the comparison:
+ *
+ *
+ * - {@link #NaN} is considered by this method to be equal to itself and greater
+ * than all other half-precision float values (including {@code #POSITIVE_INFINITY})
+ * - {@link #POSITIVE_ZERO} is considered by this method to be greater than
+ * {@link #NEGATIVE_ZERO}.
+ *
+ *
+ * @param x The first half-precision float value to compare.
+ * @param y The second half-precision float value to compare
+ *
+ * @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a
+ * value less than {@code 0} if {@code x} is numerically less than {@code y},
+ * and a value greater than {@code 0} if {@code x} is numerically greater
+ * than {@code y}
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static int compare(short x, short y) {
+ if (less(x, y)) return -1;
+ if (greater(x, y)) return 1;
+ // Collapse NaNs, akin to halfToIntBits(), but we want to keep
+ // (signed) short value types to preserve the ordering of -0.0
+ // and +0.0
+ short xBits = isNaN(x) ? NaN : x;
+ short yBits = isNaN(y) ? NaN : y;
+ return (xBits == yBits ? 0 : (xBits < yBits ? -1 : 1));
+ }
+ /**
+ * Returns the closest integral half-precision float value to the specified
+ * half-precision float value. Special values are handled in the
+ * following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The value of the specified half-precision float rounded to the nearest
+ * half-precision float value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short rint(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ if (abs > 0x3800){
+ result |= 0x3c00;
+ }
+ } else if (abs < 0x6400) {
+ int exp = 25 - (abs >> 10);
+ int mask = (1 << exp) - 1;
+ result += ((1 << (exp - 1)) - (~(abs >> exp) & 1));
+ result &= ~mask;
+ }
+ if (isNaN((short) result)) {
+ // if result is NaN mask with qNaN
+ // (i.e. mask the most significant mantissa bit with 1)
+ // to comply with hardware implementations (ARM64, Intel, etc).
+ result |= NaN;
+ }
+ return (short) result;
+ }
+ /**
+ * Returns the smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short ceil(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ result |= 0x3c00 & -(~(bits >> 15) & (abs != 0 ? 1 : 0));
+ } else if (abs < 0x6400) {
+ abs = 25 - (abs >> 10);
+ int mask = (1 << abs) - 1;
+ result += mask & ((bits >> 15) - 1);
+ result &= ~mask;
+ }
+ if (isNaN((short) result)) {
+ // if result is NaN mask with qNaN
+ // (i.e. mask the most significant mantissa bit with 1)
+ // to comply with hardware implementations (ARM64, Intel, etc).
+ result |= NaN;
+ }
+ return (short) result;
+ }
+ /**
+ * Returns the largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short floor(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ result |= 0x3c00 & (bits > 0x8000 ? 0xffff : 0x0);
+ } else if (abs < 0x6400) {
+ abs = 25 - (abs >> 10);
+ int mask = (1 << abs) - 1;
+ result += mask & -(bits >> 15);
+ result &= ~mask;
+ }
+ if (isNaN((short) result)) {
+ // if result is NaN mask with qNaN
+ // i.e. (Mask the most significant mantissa bit with 1)
+ result |= NaN;
+ }
+ return (short) result;
+ }
+ /**
+ * Returns the truncated half-precision float value of the specified
+ * half-precision float value. Special values are handled in the following ways:
+ *
+ * - If the specified half-precision float is NaN, the result is NaN
+ * - If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)
+ * - If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)
+ *
+ *
+ * @param h A half-precision float value
+ * @return The truncated half-precision float value of the specified
+ * half-precision float value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short trunc(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ } else if (abs < 0x6400) {
+ abs = 25 - (abs >> 10);
+ int mask = (1 << abs) - 1;
+ result &= ~mask;
+ }
+ return (short) result;
+ }
+ /**
+ * Returns the smaller of two half-precision float values (the value closest
+ * to negative infinity). Special values are handled in the following ways:
+ *
+ * - If either value is NaN, the result is NaN
+ * - {@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}
+ *
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ * @return The smaller of the two specified half-precision values
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short min(short x, short y) {
+ if (isNaN(x)) return NaN;
+ if (isNaN(y)) return NaN;
+ if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) {
+ return (x & SIGN_MASK) != 0 ? x : y;
+ }
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y;
+ }
+ /**
+ * Returns the larger of two half-precision float values (the value closest
+ * to positive infinity). Special values are handled in the following ways:
+ *
+ * - If either value is NaN, the result is NaN
+ * - {@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}
+ *
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return The larger of the two specified half-precision values
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short max(short x, short y) {
+ if (isNaN(x)) return NaN;
+ if (isNaN(y)) return NaN;
+ if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) {
+ return (x & SIGN_MASK) != 0 ? y : x;
+ }
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y;
+ }
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than y, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean less(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than or equal to the second half-precision
+ * float value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than or equal to y, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean lessEquals(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <=
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean greater(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than or equal to the second half-precision float
+ * value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean greaterEquals(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >=
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+ /**
+ * Returns true if the two half-precision float values are equal.
+ * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO}
+ * and {@link #NEGATIVE_ZERO} are considered equal.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is equal to y, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean equals(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+ return x == y || ((x | y) & EXPONENT_SIGNIFICAND_MASK) == 0;
+ }
+ /**
+ * Returns true if the specified half-precision float value represents
+ * infinity, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is positive infinity or negative infinity,
+ * false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean isInfinite(short h) {
+ return (h & EXPONENT_SIGNIFICAND_MASK) == POSITIVE_INFINITY;
+ }
+ /**
+ * Returns true if the specified half-precision float value represents
+ * a Not-a-Number, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is a NaN, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean isNaN(short h) {
+ return (h & EXPONENT_SIGNIFICAND_MASK) > POSITIVE_INFINITY;
+ }
+ /**
+ * Returns true if the specified half-precision float value is normalized
+ * (does not have a subnormal representation). If the specified value is
+ * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY},
+ * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal
+ * number, this method returns false.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is normalized, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static boolean isNormalized(short h) {
+ return (h & POSITIVE_INFINITY) != 0 && (h & POSITIVE_INFINITY) != POSITIVE_INFINITY;
+ }
+ /**
+ * Converts the specified half-precision float value into a
+ * single-precision float value. The following special cases are handled:
+ *
+ * - If the input is {@link #NaN}, the returned value is {@link Float#NaN}
+ * - If the input is {@link #POSITIVE_INFINITY} or
+ * {@link #NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}
+ * - If the input is 0 (positive or negative), the returned value is +/-0.0f
+ * - Otherwise, the returned value is a normalized single-precision float value
+ *
+ *
+ * @param h The half-precision float value to convert to single-precision
+ * @return A normalized single-precision float value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static float toFloat(short h) {
+ int bits = h & 0xffff;
+ int s = bits & SIGN_MASK;
+ int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
+ int m = (bits ) & SIGNIFICAND_MASK;
+ int outE = 0;
+ int outM = 0;
+ if (e == 0) { // Denormal or 0
+ if (m != 0) {
+ // Convert denorm fp16 into normalized fp32
+ float o = Float.intBitsToFloat(FP32_DENORMAL_MAGIC + m);
+ o -= FP32_DENORMAL_FLOAT;
+ return s == 0 ? o : -o;
+ }
+ } else {
+ outM = m << 13;
+ if (e == 0x1f) { // Infinite or NaN
+ outE = 0xff;
+ if (outM != 0) { // SNaNs are quieted
+ outM |= FP32_QNAN_MASK;
+ }
+ } else {
+ outE = e - EXPONENT_BIAS + FP32_EXPONENT_BIAS;
+ }
+ }
+ int out = (s << 16) | (outE << FP32_EXPONENT_SHIFT) | outM;
+ return Float.intBitsToFloat(out);
+ }
+ /**
+ * Converts the specified single-precision float value into a
+ * half-precision float value. The following special cases are handled:
+ *
+ * - If the input is NaN (see {@link Float#isNaN(float)}), the returned
+ * value is {@link #NaN}
+ * - If the input is {@link Float#POSITIVE_INFINITY} or
+ * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}
+ * - If the input is 0 (positive or negative), the returned value is
+ * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
+ * - If the input is a less than {@link #MIN_VALUE}, the returned value
+ * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}
+ * - If the input is a less than {@link #MIN_NORMAL}, the returned value
+ * is a denorm half-precision float
+ * - Otherwise, the returned value is rounded to the nearest
+ * representable half-precision float value
+ *
+ *
+ * @param f The single-precision float value to convert to half-precision
+ * @return A half-precision float value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static short toHalf(float f) {
+ int bits = Float.floatToRawIntBits(f);
+ int s = (bits >>> FP32_SIGN_SHIFT );
+ int e = (bits >>> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK;
+ int m = (bits ) & FP32_SIGNIFICAND_MASK;
+ int outE = 0;
+ int outM = 0;
+ if (e == 0xff) { // Infinite or NaN
+ outE = 0x1f;
+ outM = m != 0 ? 0x200 : 0;
+ } else {
+ e = e - FP32_EXPONENT_BIAS + EXPONENT_BIAS;
+ if (e >= 0x1f) { // Overflow
+ outE = 0x1f;
+ } else if (e <= 0) { // Underflow
+ if (e < -10) {
+ // The absolute fp32 value is less than MIN_VALUE, flush to +/-0
+ } else {
+ // The fp32 value is a normalized float less than MIN_NORMAL,
+ // we convert to a denorm fp16
+ m = m | 0x800000;
+ int shift = 14 - e;
+ outM = m >> shift;
+ int lowm = m & ((1 << shift) - 1);
+ int hway = 1 << (shift - 1);
+ // if above halfway or exactly halfway and outM is odd
+ if (lowm + (outM & 1) > hway){
+ // Round to nearest even
+ // Can overflow into exponent bit, which surprisingly is OK.
+ // This increment relies on the +outM in the return statement below
+ outM++;
+ }
+ }
+ } else {
+ outE = e;
+ outM = m >> 13;
+ // if above halfway or exactly halfway and outM is odd
+ if ((m & 0x1fff) + (outM & 0x1) > 0x1000) {
+ // Round to nearest even
+ // Can overflow into exponent bit, which surprisingly is OK.
+ // This increment relies on the +outM in the return statement below
+ outM++;
+ }
+ }
+ }
+ // The outM is added here as the +1 increments for outM above can
+ // cause an overflow in the exponent bit which is OK.
+ return (short) ((s << SIGN_SHIFT) | (outE << EXPONENT_SHIFT) + outM);
+ }
+ /**
+ * Returns a hexadecimal string representation of the specified half-precision
+ * float value. If the value is a NaN, the result is "NaN"
,
+ * otherwise the result follows this format:
+ *
+ * - If the sign is positive, no sign character appears in the result
+ * - If the sign is negative, the first character is
'-'
+ * - If the value is inifinity, the string is
"Infinity"
+ * - If the value is 0, the string is
"0x0.0p0"
+ * - If the value has a normalized representation, the exponent and
+ * significand are represented in the string in two fields. The significand
+ * starts with
"0x1."
followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by "p"
, itself followed by a decimal
+ * string of the unbiased exponent
+ * - If the value has a subnormal representation, the significand starts
+ * with
"0x0."
followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by "p-14"
+ *
+ *
+ * @param h A half-precision float value
+ * @return A hexadecimal string representation of the specified value
+ *
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static String toHexString(short h) {
+ StringBuilder o = new StringBuilder();
+ int bits = h & 0xffff;
+ int s = (bits >>> SIGN_SHIFT );
+ int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
+ int m = (bits ) & SIGNIFICAND_MASK;
+ if (e == 0x1f) { // Infinite or NaN
+ if (m == 0) {
+ if (s != 0) o.append('-');
+ o.append("Infinity");
+ } else {
+ o.append("NaN");
+ }
+ } else {
+ if (s == 1) o.append('-');
+ if (e == 0) {
+ if (m == 0) {
+ o.append("0x0.0p0");
+ } else {
+ o.append("0x0.");
+ String significand = Integer.toHexString(m);
+ o.append(significand.replaceFirst("0{2,}$", ""));
+ o.append("p-14");
+ }
+ } else {
+ o.append("0x1.");
+ String significand = Integer.toHexString(m);
+ o.append(significand.replaceFirst("0{2,}$", ""));
+ o.append('p');
+ o.append(Integer.toString(e - EXPONENT_BIAS));
+ }
+ }
+ return o.toString();
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java
new file mode 100644
index 00000000..6e20d544
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/external/android/util/Size.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.firstinspires.ftc.robotcore.external.android.util;
+
+/**
+ * Immutable class for describing width and height dimensions in integer valued units.
+ *
+ * Backported from API21, where it was introduced, to here, where we need
+ * to support API19.
+ */
+public final class Size {
+ /**
+ * Create a new immutable Size instance.
+ *
+ * @param width The width of the size, in pixels
+ * @param height The height of the size, in pixels
+ */
+ public Size(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Get the width of the size (in pixels).
+ * @return width
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Get the height of the size (in pixels).
+ * @return height
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Check if this size is equal to another size.
+ *
+ * Two sizes are equal if and only if both their widths and heights are
+ * equal.
+ *
+ *
+ * A size object is never equal to any other type of object.
+ *
+ *
+ * @return {@code true} if the objects were equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Size) {
+ Size other = (Size) obj;
+ return mWidth == other.mWidth && mHeight == other.mHeight;
+ }
+ return false;
+ }
+
+ /**
+ * Return the size represented as a string with the format {@code "WxH"}
+ *
+ * @return string representation of the size
+ */
+ @Override
+ public String toString() {
+ return mWidth + "x" + mHeight;
+ }
+
+ private static NumberFormatException invalidSize(String s) {
+ throw new NumberFormatException("Invalid Size: \"" + s + "\"");
+ }
+
+ /**
+ * Parses the specified string as a size value.
+ *
+ * The ASCII characters {@code \}{@code u002a} ('*') and
+ * {@code \}{@code u0078} ('x') are recognized as separators between
+ * the width and height.
+ *
+ * For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}.
+ * However, the method also handles sizes expressed in the
+ * following forms:
+ *
+ * "width{@code x}height" or
+ * "width{@code *}height" {@code => new Size(width, height)},
+ * where width and height are string integers potentially
+ * containing a sign, such as "-10", "+7" or "5".
+ *
+ * {@code
+ * Size.parseSize("3*+6").equals(new Size(3, 6)) == true
+ * Size.parseSize("-3x-6").equals(new Size(-3, -6)) == true
+ * Size.parseSize("4 by 3") => throws NumberFormatException
+ * }
+ *
+ * @param string the string representation of a size value.
+ * @return the size value represented by {@code string}.
+ *
+ * @throws NumberFormatException if {@code string} cannot be parsed
+ * as a size value.
+ * @throws NullPointerException if {@code string} was {@code null}
+ */
+ public static Size parseSize(String string)
+ throws NumberFormatException {
+ if (null==string) throw new IllegalArgumentException("string must not be null");
+
+ int sep_ix = string.indexOf('*');
+ if (sep_ix < 0) {
+ sep_ix = string.indexOf('x');
+ }
+ if (sep_ix < 0) {
+ throw invalidSize(string);
+ }
+ try {
+ return new Size(Integer.parseInt(string.substring(0, sep_ix)),
+ Integer.parseInt(string.substring(sep_ix + 1)));
+ } catch (NumberFormatException e) {
+ throw invalidSize(string);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
+ return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2)));
+ }
+
+ private final int mWidth;
+ private final int mHeight;
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java
new file mode 100644
index 00000000..9e85951b
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibration.java
@@ -0,0 +1,172 @@
+/*
+Copyright (c) 2018 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.internal.camera.calibration;
+
+import androidx.annotation.NonNull;
+
+import org.firstinspires.ftc.robotcore.external.android.util.Size;
+import org.firstinspires.ftc.robotcore.internal.system.AppUtil;
+import org.firstinspires.ftc.robotcore.internal.system.Assert;
+import org.firstinspires.ftc.robotcore.internal.system.Misc;
+
+/**
+ * An augmentation to {@link CameraIntrinsics} that helps support debugging
+ * and parsing from XML.
+ */
+@SuppressWarnings("WeakerAccess")
+public class CameraCalibration extends CameraIntrinsics implements Cloneable
+{
+ //----------------------------------------------------------------------------------------------
+ // State
+ //----------------------------------------------------------------------------------------------
+
+ /** These are not passed to native code */
+ protected CameraCalibrationIdentity identity;
+ protected Size size;
+ protected boolean remove;
+ protected final boolean isFake;
+
+ @Override public String toString()
+ {
+ return Misc.formatInvariant("CameraCalibration(%s %dx%d f=%.3f,%.3f)", identity, size.getWidth(), size.getHeight(), focalLengthX, focalLengthY);
+ }
+
+ public CameraCalibrationIdentity getIdentity()
+ {
+ return identity;
+ }
+ public Size getSize()
+ {
+ return size;
+ }
+ public boolean getRemove()
+ {
+ return remove;
+ }
+ public boolean isFake()
+ {
+ return isFake;
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Construction
+ //----------------------------------------------------------------------------------------------
+
+ public CameraCalibration(@NonNull CameraCalibrationIdentity identity, Size size, float focalLengthX, float focalLengthY, float principalPointX, float principalPointY, float[] distortionCoefficients, boolean remove, boolean isFake) throws RuntimeException
+ {
+ super(focalLengthX, focalLengthY, principalPointX, principalPointY, distortionCoefficients);
+ this.identity = identity;
+ this.size = size;
+ this.remove = remove;
+ this.isFake = isFake;
+ }
+
+ public CameraCalibration(@NonNull CameraCalibrationIdentity identity, int[] size, float[] focalLength, float[] principalPoint, float[] distortionCoefficients, boolean remove, boolean isFake) throws RuntimeException
+ {
+ this(identity, new Size(size[0], size[1]), focalLength[0], focalLength[1], principalPoint[0], principalPoint[1], distortionCoefficients, remove, isFake);
+ if (size.length != 2) throw Misc.illegalArgumentException("frame size must be 2");
+ if (principalPoint.length != 2) throw Misc.illegalArgumentException("principal point size must be 2");
+ if (focalLength.length != 2) throw Misc.illegalArgumentException("focal length size must be 2");
+ if (distortionCoefficients.length != 8) throw Misc.illegalArgumentException("distortion coefficients size must be 8");
+ }
+
+ public static CameraCalibration forUnavailable(CameraCalibrationIdentity calibrationIdentity, Size size)
+ {
+ if (calibrationIdentity==null)
+ {
+ calibrationIdentity = new VendorProductCalibrationIdentity(0, 0);
+ }
+ return new CameraCalibration(calibrationIdentity, size, 0, 0, 0, 0, new float[8], false, true);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ protected CameraCalibration memberwiseClone()
+ {
+ try {
+ return (CameraCalibration)super.clone();
+ }
+ catch (CloneNotSupportedException e)
+ {
+ throw AppUtil.getInstance().unreachable();
+ }
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Access
+ //----------------------------------------------------------------------------------------------
+
+ /*
+ * From: Dobrev, Niki
+ * Sent: Wednesday, May 23, 2018 4:57 AM
+ *
+ * Other option (in case the aspect ratio stays the same) is to just scale the principal point
+ * and focal length values to match the resolution currently used before providing them to
+ * Vuforia in the camera frame callback. Please keep in mind, that this is not optimal and it
+ * is possible that it doesn't provide optimal results always, but from calibration effort point
+ * of view is probably easier than doing calibrations for each supported resolution. Scaling
+ * should work reasonably well in general case, if the camera is not doing anything strange when
+ * switching capture resolutions.
+ */
+ public CameraCalibration scaledTo(Size newSize)
+ {
+ Assert.assertTrue(Misc.approximatelyEquals(getAspectRatio(newSize), getAspectRatio(size)));
+ double factor = (double)(newSize.getWidth()) / (double)(size.getWidth());
+
+ CameraCalibration result = memberwiseClone();
+ result.size = newSize;
+ result.focalLengthX *= factor;
+ result.focalLengthY *= factor;
+ result.principalPointX *= factor;
+ result.principalPointY *= factor;
+ return result;
+ }
+
+ public double getAspectRatio()
+ {
+ return getAspectRatio(size);
+ }
+ public double getDiagonal()
+ {
+ return getDiagonal(size);
+ }
+
+ public static double getDiagonal(Size size)
+ {
+ return Math.sqrt(size.getWidth() * size.getWidth() + size.getHeight() * size.getHeight());
+ }
+ protected static double getAspectRatio(Size size)
+ {
+ return (double)size.getWidth() / (double)size.getHeight();
+ }
+
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java
new file mode 100644
index 00000000..883db001
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraCalibrationIdentity.java
@@ -0,0 +1,38 @@
+/*
+Copyright (c) 2018 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.internal.camera.calibration;
+
+public interface CameraCalibrationIdentity
+{
+ boolean isDegenerate();
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java
new file mode 100644
index 00000000..2b609d45
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/CameraIntrinsics.java
@@ -0,0 +1,117 @@
+/*
+Copyright (c) 2018 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.internal.camera.calibration;
+
+import androidx.annotation.NonNull;
+
+import org.firstinspires.ftc.robotcore.internal.system.Misc;
+
+import java.util.Arrays;
+
+/**
+ * Provides basic information regarding some characteristics which are built-in to / intrinsic
+ * to a particular camera model.
+ *
+ * Note that this information is passed to native code. This class is a Java manifestation of
+ * of Vuforia::ExternalProvider::CameraIntrinsics found in ExternalProvider.h.
+ *
+ * https://docs.opencv.org/2.4/doc/tutorials/calib3d/camera_calibration/camera_calibration.html
+ * https://docs.opencv.org/3.0-beta/doc/tutorials/calib3d/camera_calibration/camera_calibration.html
+ * https://www.mathworks.com/help/vision/camera-calibration.html
+ *
+ * @see CameraCalibration
+ */
+@SuppressWarnings("WeakerAccess")
+public class CameraIntrinsics
+{
+ /** Focal length x-component. 0.f if not available. */
+ public float focalLengthX;
+
+ /** Focal length y-component. 0.f if not available. */
+ public float focalLengthY;
+
+ /** Principal point x-component. 0.f if not available. */
+ public float principalPointX;
+
+ /** Principal point y-component. 0.f if not available. */
+ public float principalPointY;
+
+ /**
+ * An 8 element array of distortion coefficients.
+ * Array should be filled in the following order (r: radial, t:tangential):
+ * [r0, r1, t0, t1, r2, r3, r4, r5]
+ * Values that are not available should be set to 0.f.
+ *
+ * Yes, the parameter order seems odd, but it is correct.
+ */
+ @NonNull public final float[] distortionCoefficients;
+
+ public CameraIntrinsics(float focalLengthX, float focalLengthY, float principalPointX, float principalPointY, float[] distortionCoefficients) throws RuntimeException
+ {
+ if (distortionCoefficients != null && distortionCoefficients.length == 8)
+ {
+ this.focalLengthX = focalLengthX;
+ this.focalLengthY = focalLengthY;
+ this.principalPointX = principalPointX;
+ this.principalPointY = principalPointY;
+ this.distortionCoefficients = Arrays.copyOf(distortionCoefficients, distortionCoefficients.length);
+ }
+ else
+ throw Misc.illegalArgumentException("distortionCoefficients must have length 8");
+ }
+
+ public float[] toArray()
+ {
+ float[] result = new float[12];
+ result[0] = focalLengthX;
+ result[1] = focalLengthY;
+ result[2] = principalPointX;
+ result[3] = principalPointY;
+ System.arraycopy(distortionCoefficients, 0, result, 4, 8);
+ return result;
+ }
+
+ public boolean isDegenerate()
+ {
+ return focalLengthX==0 && focalLengthY==0 && principalPointX==0 && principalPointY==0
+ && distortionCoefficients[0]==0
+ && distortionCoefficients[1]==0
+ && distortionCoefficients[2]==0
+ && distortionCoefficients[3]==0
+ && distortionCoefficients[4]==0
+ && distortionCoefficients[5]==0
+ && distortionCoefficients[6]==0
+ && distortionCoefficients[7]==0;
+ }
+
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java
new file mode 100644
index 00000000..4d323b4c
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/camera/calibration/VendorProductCalibrationIdentity.java
@@ -0,0 +1,84 @@
+/*
+Copyright (c) 2018 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.internal.camera.calibration;
+
+import org.firstinspires.ftc.robotcore.internal.system.Misc;
+
+public class VendorProductCalibrationIdentity implements CameraCalibrationIdentity
+{
+ //----------------------------------------------------------------------------------------------
+ // State
+ //----------------------------------------------------------------------------------------------
+
+ public final int vid;
+ public final int pid;
+
+ @Override public String toString()
+ {
+ return Misc.formatInvariant("%s(vid=0x%04x,pid=0x%04x)", getClass().getSimpleName(), vid, pid);
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Construction
+ //----------------------------------------------------------------------------------------------
+
+ public VendorProductCalibrationIdentity(int vid, int pid)
+ {
+ this.vid = vid;
+ this.pid = pid;
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Comparison
+ //----------------------------------------------------------------------------------------------
+
+ @Override public boolean isDegenerate()
+ {
+ return vid==0 || pid==0;
+ }
+
+ @Override public boolean equals(Object o)
+ {
+ if (o instanceof VendorProductCalibrationIdentity)
+ {
+ VendorProductCalibrationIdentity them = (VendorProductCalibrationIdentity)o;
+ return vid==them.vid && pid==them.pid;
+ }
+ return super.equals(o);
+ }
+
+ @Override public int hashCode()
+ {
+ return Integer.valueOf(vid).hashCode() ^ Integer.valueOf(pid).hashCode() ^ 738187;
+ }
+}
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java
index e64f66a6..895a2004 100644
--- a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java
@@ -201,4 +201,15 @@ public int drainTo(Collection super E> c, int maxElements) {
return targetQueue.drainTo(c, maxElements);
}
}
+
+ @Override
+ public void clear() {
+ synchronized (theLock) {
+ for(E e : targetQueue)
+ if (evictAction != null)
+ evictAction.accept(e);
+
+ targetQueue.clear();
+ }
+ }
}
\ No newline at end of file
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java
new file mode 100644
index 00000000..62eda152
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/AppUtil.java
@@ -0,0 +1,70 @@
+package org.firstinspires.ftc.robotcore.internal.system;
+
+import com.qualcomm.robotcore.util.RobotLog;
+
+public class AppUtil {
+
+ public static final String TAG= "AppUtil";
+
+ protected AppUtil() { }
+
+ private static AppUtil instance = null;
+
+ public static AppUtil getInstance() {
+ if(instance == null) {
+ instance = new AppUtil();
+ }
+
+ return instance;
+ }
+
+ public RuntimeException unreachable()
+ {
+ return unreachable(TAG);
+ }
+
+ public RuntimeException unreachable(Throwable throwable)
+ {
+ return unreachable(TAG, throwable);
+ }
+
+ public RuntimeException unreachable(String tag)
+ {
+ return failFast(tag, "internal error: this code is unreachable");
+ }
+
+ public RuntimeException unreachable(String tag, Throwable throwable)
+ {
+ return failFast(tag, throwable, "internal error: this code is unreachable");
+ }
+
+ public RuntimeException failFast(String tag, String format, Object... args)
+ {
+ String message = String.format(format, args);
+ return failFast(tag, message);
+ }
+
+ public RuntimeException failFast(String tag, String message)
+ {
+ RobotLog.ee(tag, message);
+ exitApplication(-1);
+ return new RuntimeException("keep compiler happy");
+ }
+
+ public RuntimeException failFast(String tag, Throwable throwable, String format, Object... args)
+ {
+ String message = String.format(format, args);
+ return failFast(tag, throwable, message);
+ }
+
+ public RuntimeException failFast(String tag, Throwable throwable, String message)
+ {
+ RobotLog.ee(tag, throwable, message);
+ exitApplication(-1);
+ return new RuntimeException("keep compiler happy", throwable);
+ }
+
+ public void exitApplication(int code) {
+ System.exit(code);
+ }
+}
diff --git a/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java
new file mode 100644
index 00000000..a720625b
--- /dev/null
+++ b/Common/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Misc.java
@@ -0,0 +1,475 @@
+/*
+Copyright (c) 2017 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.internal.system;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
+import java.lang.reflect.Array;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * A collection of misfit utilities. They all need to go somewhere, and we can't
+ * seem to find a better fit.
+ */
+@SuppressWarnings("WeakerAccess")
+public class Misc
+{
+ public static final String TAG = "Misc";
+
+ //----------------------------------------------------------------------------------------------
+ // Strings
+ //----------------------------------------------------------------------------------------------
+
+ /** Formats the string using what in C# is called the 'invariant' culture */
+ public static String formatInvariant(@NonNull String format, Object...args)
+ {
+ return String.format(Locale.ROOT, format, args);
+ }
+ public static String formatInvariant(@NonNull String format)
+ {
+ return format;
+ }
+
+ public static String formatForUser(@NonNull String format, Object...args)
+ {
+ return String.format(Locale.getDefault(), format, args);
+ }
+ public static String formatForUser(@NonNull String format)
+ {
+ return format;
+ }
+
+ public static String formatForUser(@StringRes int resId, Object...args)
+ {
+ // return AppUtil.getDefContext().getString(resId, args);
+ throw new RuntimeException("Stub!");
+ }
+ public static String formatForUser(@StringRes int resId)
+ {
+ // return AppUtil.getDefContext().getString(resId);
+ throw new RuntimeException("Stub!");
+ }
+
+ public static String encodeEntity(String string)
+ {
+ return encodeEntity(string, "");
+ }
+ public static String encodeEntity(String string, String rgchEscape)
+ {
+ StringBuilder builder = new StringBuilder();
+ for (char ch : string.toCharArray())
+ {
+ switch (ch)
+ {
+ case '&':
+ builder.append("&");
+ break;
+ case '<':
+ builder.append("<");
+ break;
+ case '>':
+ builder.append(">");
+ break;
+ case '"':
+ builder.append(""");
+ break;
+ case '\'':
+ builder.append("'");
+ break;
+ default:
+ if (rgchEscape.indexOf(ch) >= 0)
+ builder.append(Misc.formatInvariant("%x;", ch));
+ else
+ builder.append(ch);
+ break;
+ }
+ }
+ return builder.toString();
+ }
+ public static String decodeEntity(String string)
+ {
+ StringBuilder builder = new StringBuilder();
+ for (int ich = 0; ich < string.length(); ich++)
+ {
+ char ch = string.charAt(ich);
+ if (ch == '&')
+ {
+ ich++;
+ int ichFirst = ich;
+ while (string.charAt(ich) != ';')
+ {
+ ich++;
+ }
+ String payload = string.substring(ichFirst, ich-1);
+ switch (payload)
+ {
+ case "amp":
+ builder.append('&');
+ break;
+ case "lt":
+ builder.append('<');
+ break;
+ case "gt":
+ builder.append('>');
+ break;
+ case "quot":
+ builder.append('"');
+ break;
+ case "apos":
+ builder.append('\'');
+ break;
+ default:
+ if (payload.length() > 2 && payload.charAt(0) == '#' && payload.charAt(1) == 'x')
+ {
+ payload = "0x" + payload.substring(2);
+ int i = Integer.decode(payload);
+ builder.append((char)i);
+ }
+ else
+ throw illegalArgumentException("illegal entity reference");
+ }
+ }
+ else
+ builder.append(ch);
+ }
+ return builder.toString();
+ }
+ //----------------------------------------------------------------------------------------------
+ // Math
+ //----------------------------------------------------------------------------------------------
+
+ public static long saturatingAdd(long x, long y)
+ {
+ if (x == 0 || y == 0 || (x > 0 ^ y > 0))
+ {
+ //zero+N or one pos, another neg = no problems
+ return x + y;
+ }
+ else if (x > 0)
+ {
+ //both pos, can only overflow
+ return Long.MAX_VALUE - x < y ? Long.MAX_VALUE : x + y;
+ }
+ else
+ {
+ //both neg, can only underflow
+ return Long.MIN_VALUE - x > y ? Long.MIN_VALUE : x + y;
+ }
+ }
+ public static int saturatingAdd(int x, int y)
+ {
+ if (x == 0 || y == 0 || (x > 0 ^ y > 0))
+ {
+ //zero+N or one pos, another neg = no problems
+ return x + y;
+ }
+ else if (x > 0)
+ {
+ //both pos, can only overflow
+ return Integer.MAX_VALUE - x < y ? Integer.MAX_VALUE : x + y;
+ }
+ else
+ {
+ //both neg, can only underflow
+ return Integer.MIN_VALUE - x > y ? Integer.MIN_VALUE : x + y;
+ }
+ }
+
+ public static boolean isEven(byte value)
+ {
+ return (value & 1) == 0;
+ }
+ public static boolean isEven(short value)
+ {
+ return (value & 1) == 0;
+ }
+ public static boolean isEven(int value)
+ {
+ return (value & 1) == 0;
+ }
+ public static boolean isEven(long value)
+ {
+ return (value & 1) == 0;
+ }
+
+ public static boolean isOdd(byte value)
+ {
+ return !isEven(value);
+ }
+ public static boolean isOdd(short value)
+ {
+ return !isEven(value);
+ }
+ public static boolean isOdd(int value)
+ {
+ return !isEven(value);
+ }
+ public static boolean isOdd(long value)
+ {
+ return !isEven(value);
+ }
+
+ public static boolean isFinite(double d)
+ {
+ return !Double.isNaN(d) && !Double.isInfinite(d);
+ }
+
+ public static boolean approximatelyEquals(double a, double b)
+ {
+ return approximatelyEquals(a, b, 1e-9);
+ }
+
+ public static boolean approximatelyEquals(double a, double b, double tolerance)
+ {
+ if (a==b) return true; // zero and infinity are the important cases
+ double error = b==0 ? Math.abs(a) : Math.abs(a/b-1.0); // pretty arbitrary
+ return error < tolerance;
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // UUIDs
+ //----------------------------------------------------------------------------------------------
+
+ /** @see UUID Spec */
+ public static UUID uuidFromBytes(byte[] rgb, ByteOrder byteOrder)
+ {
+ Assert.assertTrue(rgb.length == 16);
+
+ ByteBuffer readBuffer = ByteBuffer.wrap(rgb).order(byteOrder);
+ ByteBuffer writeBuffer = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
+
+ // There's funky byte ordering in the first eight bytes
+ writeBuffer.putInt(readBuffer.getInt());
+ writeBuffer.putShort(readBuffer.getShort());
+ writeBuffer.putShort(readBuffer.getShort());
+ writeBuffer.rewind();
+ long mostSignificant = writeBuffer.getLong();
+
+ // The remaining eight bytes are unordered
+ writeBuffer.rewind();
+ writeBuffer.put(readBuffer);
+ writeBuffer.rewind();
+ long leastSignificant = writeBuffer.getLong();
+
+ return new UUID(mostSignificant, leastSignificant);
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Arrays
+ //----------------------------------------------------------------------------------------------
+
+ public static boolean contains(byte[] array, byte value)
+ {
+ for (byte i : array)
+ {
+ if (i == value) return true;
+ }
+ return false;
+ }
+
+ public static boolean contains(short[] array, short value)
+ {
+ for (short i : array)
+ {
+ if (i == value) return true;
+ }
+ return false;
+ }
+
+ public static boolean contains(int[] array, int value)
+ {
+ for (int i : array)
+ {
+ if (i == value) return true;
+ }
+ return false;
+ }
+
+ public static boolean contains(long[] array, long value)
+ {
+ for (long i : array)
+ {
+ if (i == value) return true;
+ }
+ return false;
+ }
+
+ public static T[] toArray(T[] contents, Collection collection)
+ {
+ int s = collection.size();
+ if (contents.length < s)
+ {
+ @SuppressWarnings("unchecked") T[] newArray = (T[]) Array.newInstance(contents.getClass().getComponentType(), s);
+ contents = newArray;
+ }
+ int i = 0;
+ for (T t : collection)
+ {
+ contents[i++] = t;
+ }
+ if (contents.length > s)
+ {
+ contents[s] = null;
+ }
+ return contents;
+ }
+
+ public static T[] toArray(T[] contents, ArrayList collection)
+ {
+ return collection.toArray(contents);
+ }
+
+ public static long[] toLongArray(Collection collection)
+ {
+ long[] result = new long[collection.size()];
+ int i = 0;
+ for (Long value: collection)
+ {
+ result[i++] = value;
+ }
+ return result;
+ }
+
+ public static int[] toIntArray(Collection collection)
+ {
+ int[] result = new int[collection.size()];
+ int i = 0;
+ for (Integer value : collection)
+ {
+ result[i++] = value;
+ }
+ return result;
+ }
+
+ public static short[] toShortArray(Collection collection)
+ {
+ short[] result = new short[collection.size()];
+ int i = 0;
+ for (Short value : collection)
+ {
+ result[i++] = value;
+ }
+ return result;
+ }
+
+ public static byte[] toByteArray(Collection collection)
+ {
+ byte[] result = new byte[collection.size()];
+ int i = 0;
+ for (Byte value: collection)
+ {
+ result[i++] = value;
+ }
+ return result;
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Collections
+ //----------------------------------------------------------------------------------------------
+
+ public static Set intersect(Set left, Set right)
+ {
+ Set result = new HashSet<>();
+ for (E element : left)
+ {
+ if (right.contains(element))
+ {
+ result.add(element);
+ }
+ }
+ return result;
+ }
+
+ //----------------------------------------------------------------------------------------------
+ // Exceptions
+ //----------------------------------------------------------------------------------------------
+
+ public static IllegalArgumentException illegalArgumentException(String message)
+ {
+ return new IllegalArgumentException(message);
+ }
+ public static IllegalArgumentException illegalArgumentException(String format, Object...args)
+ {
+ return new IllegalArgumentException(formatInvariant(format, args));
+ }
+ public static IllegalArgumentException illegalArgumentException(Throwable throwable, String format, Object...args)
+ {
+ return new IllegalArgumentException(formatInvariant(format, args), throwable);
+ }
+ public static IllegalArgumentException illegalArgumentException(Throwable throwable, String message)
+ {
+ return new IllegalArgumentException(message, throwable);
+ }
+
+ public static IllegalStateException illegalStateException(String message)
+ {
+ return new IllegalStateException(message);
+ }
+ public static IllegalStateException illegalStateException(String format, Object...args)
+ {
+ return new IllegalStateException(formatInvariant(format, args));
+ }
+ public static IllegalStateException illegalStateException(Throwable throwable, String format, Object...args)
+ {
+ return new IllegalStateException(formatInvariant(format, args), throwable);
+ }
+ public static IllegalStateException illegalStateException(Throwable throwable, String message)
+ {
+ return new IllegalStateException(message, throwable);
+ }
+
+ public static RuntimeException internalError(String message)
+ {
+ return new RuntimeException("internal error:" + message);
+ }
+ public static RuntimeException internalError(String format, Object...args)
+ {
+ return new RuntimeException("internal error:" + formatInvariant(format, args));
+ }
+ public static RuntimeException internalError(Throwable throwable, String format, Object...args)
+ {
+ return new RuntimeException("internal error:" + formatInvariant(format, args), throwable);
+ }
+ public static RuntimeException internalError(Throwable throwable, String message)
+ {
+ return new RuntimeException("internal error:" + message, throwable);
+ }
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java
index 0acc74d9..6519bf04 100644
--- a/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java
+++ b/Common/src/main/java/org/openftc/easyopencv/MatRecycler.java
@@ -54,18 +54,27 @@ public MatRecycler(int num) {
this(num, 0, 0, CvType.CV_8UC3);
}
- public synchronized RecyclableMat takeMat() {
+ public synchronized RecyclableMat takeMatOrNull() {
if (availableMats.size() == 0) {
- throw new RuntimeException("All mats have been checked out!");
+ return null;
}
- RecyclableMat mat = null;
try {
- mat = availableMats.take();
- mat.checkedOut = true;
+ return takeMatOrInterrupt();
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ return null;
}
+ }
+
+ public synchronized RecyclableMat takeMatOrInterrupt() throws InterruptedException {
+ if(availableMats.size() == 0) {
+ throw new RuntimeException("All mats have been checked out!");
+ }
+
+ RecyclableMat mat;
+
+ mat = availableMats.take();
+ mat.checkedOut = true;
return mat;
}
@@ -109,6 +118,18 @@ private RecyclableMat(int idx, int rows, int cols, int type) {
this.idx = idx;
}
+ private Object context;
+
+ public void setContext(Object context)
+ {
+ this.context = context;
+ }
+
+ public Object getContext()
+ {
+ return context;
+ }
+
public void returnMat() {
synchronized(MatRecycler.this) {
try {
@@ -121,5 +142,12 @@ public void returnMat() {
public boolean isCheckedOut() { return checkedOut; }
+ @Override
+ public void copyTo(Mat mat) {
+ super.copyTo(mat);
+ if(mat instanceof RecyclableMat) {
+ ((RecyclableMat) mat).setContext(getContext());
+ }
+ }
}
}
diff --git a/Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java
deleted file mode 100644
index c108e94b..00000000
--- a/Common/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.openftc.easyopencv;
-
-import org.opencv.core.Mat;
-
-public abstract class OpenCvPipeline {
-
- public abstract Mat processFrame(Mat input);
-
- public void onViewportTapped() { }
-
- public void init(Mat mat) { }
-
-}
\ No newline at end of file
diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle
index 1284e5e3..f206f3c2 100644
--- a/EOCV-Sim/build.gradle
+++ b/EOCV-Sim/build.gradle
@@ -3,7 +3,6 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
plugins {
- id 'java'
id 'org.jetbrains.kotlin.jvm'
id 'com.github.johnrengelman.shadow'
id 'maven-publish'
@@ -34,9 +33,10 @@ test {
apply from: '../test-logging.gradle'
dependencies {
- implementation project(':Common')
+ api project(':Common')
+ api project(':Vision')
- implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib'
implementation "org.eclipse.jdt:ecj:3.21.0"
@@ -48,7 +48,7 @@ dependencies {
implementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
implementation "com.github.deltacv:steve:1.0.0"
- implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version"
+ implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version"
implementation 'info.picocli:picocli:4.6.1'
implementation 'com.google.code.gson:gson:2.8.7'
@@ -96,4 +96,4 @@ task(writeBuildClassJava) {
"}"
}
-build.dependsOn writeBuildClassJava
+build.dependsOn writeBuildClassJava
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt
index 996964c6..fd5b8000 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt
@@ -32,6 +32,7 @@ import com.github.serivesmejia.eocvsim.input.InputSourceManager
import com.github.serivesmejia.eocvsim.output.VideoRecordingSession
import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
import com.github.serivesmejia.eocvsim.pipeline.PipelineSource
+import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator
import com.github.serivesmejia.eocvsim.tuner.TunerManager
import com.github.serivesmejia.eocvsim.util.ClasspathScan
import com.github.serivesmejia.eocvsim.util.FileFilters
@@ -45,8 +46,13 @@ import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter
import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder
import com.github.serivesmejia.eocvsim.util.loggerFor
import com.github.serivesmejia.eocvsim.workspace.WorkspaceManager
+import com.qualcomm.robotcore.eventloop.opmode.OpMode
+import com.qualcomm.robotcore.eventloop.opmode.OpModePipelineHandler
+import io.github.deltacv.vision.external.PipelineRenderHook
import nu.pattern.OpenCV
+import org.opencv.core.Mat
import org.opencv.core.Size
+import org.openftc.easyopencv.TimestampedPipelineHandler
import java.awt.Dimension
import java.io.File
import javax.swing.SwingUtilities
@@ -58,9 +64,12 @@ class EOCVSim(val params: Parameters = Parameters()) {
companion object {
const val VERSION = Build.versionString
+
const val DEFAULT_EOCV_WIDTH = 320
const val DEFAULT_EOCV_HEIGHT = 240
- @JvmField val DEFAULT_EOCV_SIZE = Size(DEFAULT_EOCV_WIDTH.toDouble(), DEFAULT_EOCV_HEIGHT.toDouble())
+
+ @JvmField
+ val DEFAULT_EOCV_SIZE = Size(DEFAULT_EOCV_WIDTH.toDouble(), DEFAULT_EOCV_HEIGHT.toDouble())
private var hasScanned = false
private val classpathScan = ClasspathScan()
@@ -82,11 +91,13 @@ class EOCVSim(val params: Parameters = Parameters()) {
try {
System.load(alternativeNative.absolutePath)
+ Mat().release() //test if native lib is loaded correctly
+
isNativeLibLoaded = true
logger.info("Successfully loaded the OpenCV native lib from specified path")
return
- } catch(ex: Throwable) {
+ } catch (ex: Throwable) {
logger.error("Failure loading the OpenCV native lib from specified path", ex)
logger.info("Retrying with loadLocally...")
}
@@ -116,12 +127,19 @@ class EOCVSim(val params: Parameters = Parameters()) {
@JvmField
val configManager = ConfigManager()
+
@JvmField
val inputSourceManager = InputSourceManager(this)
+
@JvmField
- val pipelineManager = PipelineManager(this)
+ val pipelineStatisticsCalculator = PipelineStatisticsCalculator()
+
+ @JvmField
+ val pipelineManager = PipelineManager(this, pipelineStatisticsCalculator)
+
@JvmField
val tunerManager = TunerManager(this)
+
@JvmField
val workspaceManager = WorkspaceManager(this)
@@ -138,6 +156,7 @@ class EOCVSim(val params: Parameters = Parameters()) {
private val hexCode = Integer.toHexString(hashCode())
private var isRestarting = false
+ private var destroying = false
enum class DestroyReason {
USER_REQUESTED, RESTART, CRASH
@@ -146,10 +165,9 @@ class EOCVSim(val params: Parameters = Parameters()) {
fun init() {
eocvSimThread = Thread.currentThread()
- if(!EOCVSimFolder.couldLock) {
+ if (!EOCVSimFolder.couldLock) {
logger.error(
- "Couldn't finally claim lock file in \"${EOCVSimFolder.absolutePath}\"! " +
- "Is the folder opened by another EOCV-Sim instance?"
+ "Couldn't finally claim lock file in \"${EOCVSimFolder.absolutePath}\"! " + "Is the folder opened by another EOCV-Sim instance?"
)
logger.error("Unable to continue with the execution, the sim will exit now.")
@@ -167,7 +185,7 @@ class EOCVSim(val params: Parameters = Parameters()) {
//loading native lib only once in the app runtime
loadOpenCvLib(params.opencvNativeLibrary)
- if(!hasScanned) {
+ if (!hasScanned) {
classpathScan.asyncScan()
hasScanned = true
}
@@ -179,7 +197,9 @@ class EOCVSim(val params: Parameters = Parameters()) {
visualizer.initAsync(configManager.config.simTheme) //create gui in the EDT
inputSourceManager.init() //loading user created input sources
+
pipelineManager.init() //init pipeline manager (scan for pipelines)
+
tunerManager.init() //init tunable variables manager
//shows a warning when a pipeline gets "stuck"
@@ -187,42 +207,86 @@ class EOCVSim(val params: Parameters = Parameters()) {
visualizer.asyncPleaseWaitDialog(
"Current pipeline took too long to ${pipelineManager.lastPipelineAction}",
"Falling back to DefaultPipeline",
- "Close", Dimension(310, 150), true, true
+ "Close",
+ Dimension(310, 150),
+ true,
+ true
)
}
inputSourceManager.inputSourceLoader.saveInputSourcesToFile()
- visualizer.waitForFinishingInit()
+ visualizer.joinInit()
+
+ pipelineManager.subscribePipelineHandler(TimestampedPipelineHandler())
+ pipelineManager.subscribePipelineHandler(OpModePipelineHandler(inputSourceManager, visualizer.viewport))
visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one
visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0
visualizer.sourceSelectorPanel.allowSourceSwitching = true
- visualizer.pipelineSelectorPanel.updatePipelinesList() //update pipelines and pick first one (DefaultPipeline)
- visualizer.pipelineSelectorPanel.selectedIndex = 0
+ visualizer.pipelineOpModeSwitchablePanel.updateSelectorListsBlocking()
+
+ visualizer.pipelineSelectorPanel.selectedIndex = 0 //update pipelines and pick first one (DefaultPipeline)
+ visualizer.opModeSelectorPanel.selectedIndex = 0 //update opmodes and pick first one (DefaultPipeline)
+
+ visualizer.pipelineOpModeSwitchablePanel.enableSwitchingBlocking()
//post output mats from the pipeline to the visualizer viewport
- pipelineManager.pipelineOutputPosters.add(visualizer.viewport.matPoster)
+ pipelineManager.pipelineOutputPosters.add(visualizer.viewport)
+
+ // now that we have two different runnable units (OpenCvPipeline and OpMode)
+ // we have to give a more special treatment to the OpenCvPipeline
+ // OpModes can take care of themselves, setting up their own stuff
+ // but we need to do some hand holding for OpenCvPipelines...
+ pipelineManager.onPipelineChange {
+ pipelineStatisticsCalculator.init()
+
+ if(pipelineManager.currentPipeline !is OpMode && pipelineManager.currentPipeline != null) {
+ visualizer.viewport.activate()
+ visualizer.viewport.setRenderHook(PipelineRenderHook) // calls OpenCvPipeline#onDrawFrame on the viewport (UI) thread
+ } else {
+ // opmodes are on their own, lol
+ visualizer.viewport.deactivate()
+ visualizer.viewport.clearViewport()
+ }
+ }
+
+ pipelineManager.onUpdate {
+ if(pipelineManager.currentPipeline !is OpMode && pipelineManager.currentPipeline != null) {
+ visualizer.viewport.notifyStatistics(
+ pipelineStatisticsCalculator.avgFps,
+ pipelineStatisticsCalculator.avgPipelineTime,
+ pipelineStatisticsCalculator.avgOverheadTime
+ )
+ }
+
+
+ updateVisualizerTitle() // update current pipeline in title
+ }
start()
}
private fun start() {
+ if(Thread.currentThread() != eocvSimThread) {
+ throw IllegalStateException("start() must be called from the EOCVSim thread")
+ }
+
logger.info("-- Begin EOCVSim loop ($hexCode) --")
- while (!eocvSimThread.isInterrupted) {
+ while (!eocvSimThread.isInterrupted && !destroying) {
//run all pending requested runnables
onMainUpdate.run()
- updateVisualizerTitle()
+ pipelineStatisticsCalculator.newInputFrameStart()
inputSourceManager.update(pipelineManager.paused)
tunerManager.update()
try {
pipelineManager.update(
- if(inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) {
+ if (inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) {
inputSourceManager.lastMatFromSource
} else null
)
@@ -232,7 +296,8 @@ class EOCVSim(val params: Parameters = Parameters()) {
"To avoid further issues, EOCV-Sim will exit now.",
"Ok",
Dimension(450, 150),
- true, true
+ true,
+ true
).onCancel {
destroy(DestroyReason.CRASH) //destroy eocv sim when pressing "exit"
}
@@ -253,21 +318,24 @@ class EOCVSim(val params: Parameters = Parameters()) {
}
break //bye bye
+ } catch (ex: InterruptedException) {
+ break // bye bye
}
//limit FPG
fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble()
try {
fpsLimiter.sync()
- } catch(e: InterruptedException) {
+ } catch (e: InterruptedException) {
break
}
}
logger.warn("Main thread interrupted ($hexCode)")
- if(isRestarting) {
- isRestarting = false
+ if (isRestarting) {
+ Thread.interrupted() //clear interrupted flag
+
EOCVSim(params).init()
}
}
@@ -287,9 +355,9 @@ class EOCVSim(val params: Parameters = Parameters()) {
visualizer.close()
eocvSimThread.interrupt()
+ destroying = true
- if(reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH)
- jvmMainThread.interrupt()
+ if (reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) jvmMainThread.interrupt()
}
fun destroy() {
@@ -331,24 +399,22 @@ class EOCVSim(val params: Parameters = Parameters()) {
logger.info("Recording session stopped")
DialogFactory.createFileChooser(
- visualizer.frame,
- DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, FileFilters.recordedVideoFilter
+ visualizer.frame, DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, FileFilters.recordedVideoFilter
).addCloseListener { _: Int, file: File?, selectedFileFilter: FileFilter? ->
onMainUpdate.doOnce {
if (file != null) {
-
- var correctedFile = File(file.absolutePath)
+ var correctedFile = file
val extension = SysUtil.getExtensionByStringHandling(file.name)
if (selectedFileFilter is FileNameExtensionFilter) { //if user selected an extension
//get selected extension
- correctedFile = file + "." + selectedFileFilter.extensions[0]
+ correctedFile = File(file.absolutePath + "." + selectedFileFilter.extensions[0])
} else if (extension.isPresent) {
if (!extension.get().equals("avi", true)) {
- correctedFile = file + ".avi"
+ correctedFile = File(file.absolutePath + ".avi")
}
} else {
- correctedFile = file + ".avi"
+ correctedFile = File(file.absolutePath + ".avi")
}
if (correctedFile.exists()) {
@@ -378,12 +444,10 @@ class EOCVSim(val params: Parameters = Parameters()) {
val workspaceMsg = " - ${workspaceManager.workspaceFile.absolutePath} $isBuildRunning"
- val pipelineFpsMsg = " (${pipelineManager.pipelineFpsCounter.fps} Pipeline FPS)"
- val posterFpsMsg = " (${visualizer.viewport.matPoster.fpsCounter.fps} Viewport FPS)"
val isPaused = if (pipelineManager.paused) " (Paused)" else ""
val isRecording = if (isCurrentlyRecording()) " RECORDING" else ""
- val msg = isRecording + pipelineFpsMsg + posterFpsMsg + isPaused
+ val msg = isRecording + isPaused
if (pipelineManager.currentPipeline == null) {
visualizer.setTitleMessage("No pipeline$msg${workspaceMsg}")
@@ -401,4 +465,4 @@ class EOCVSim(val params: Parameters = Parameters()) {
var opencvNativeLibrary: File? = null
}
-}
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt
index 0ec06269..d10ca12e 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt
@@ -1,8 +1,8 @@
@file:JvmName("Main")
+
package com.github.serivesmejia.eocvsim
import com.github.serivesmejia.eocvsim.pipeline.PipelineSource
-import com.github.serivesmejia.eocvsim.util.loggerForThis
import picocli.CommandLine
import java.io.File
import java.nio.file.Paths
@@ -22,31 +22,48 @@ fun main(args: Array) {
@CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString])
class EOCVSimCommandInterface : Runnable {
- @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"])
- @JvmField var workspacePath: String? = null
-
- @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"])
- @JvmField var initialPipeline: String? = null
- @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"])
- @JvmField var initialPipelineSource = PipelineSource.CLASSPATH
-
- @CommandLine.Option(names = ["-o", "--opencvpath"], description = ["Specifies an alternative path for the OpenCV native to be loaded at runtime"])
- @JvmField var opencvNativePath: String? = null
+ @CommandLine.Option(
+ names = ["-w", "--workspace"],
+ description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"]
+ )
+ @JvmField
+ var workspacePath: String? = null
+
+ @CommandLine.Option(
+ names = ["-p", "--pipeline"],
+ description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]
+ )
+ @JvmField
+ var initialPipeline: String? = null
+
+ @CommandLine.Option(
+ names = ["-s", "--source"],
+ description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]
+ )
+ @JvmField
+ var initialPipelineSource = PipelineSource.CLASSPATH
+
+ @CommandLine.Option(
+ names = ["-o", "--opencvpath"],
+ description = ["Specifies an alternative path for the OpenCV native to be loaded at runtime"]
+ )
+ @JvmField
+ var opencvNativePath: String? = null
override fun run() {
val parameters = EOCVSim.Parameters()
- if(workspacePath != null) {
+ if (workspacePath != null) {
parameters.initialWorkspace = checkPath("Workspace", workspacePath!!, true)
}
- if(initialPipeline != null) {
+ if (initialPipeline != null) {
parameters.initialPipelineName = initialPipeline
parameters.initialPipelineSource = initialPipelineSource
}
- if(opencvNativePath != null) {
+ if (opencvNativePath != null) {
parameters.opencvNativeLibrary = checkPath("OpenCV Native", opencvNativePath!!, false)
}
@@ -56,16 +73,16 @@ class EOCVSimCommandInterface : Runnable {
private fun checkPath(parameter: String, path: String, shouldBeDirectory: Boolean): File {
var file = File(path)
- if(!file.exists()) {
+ if (!file.exists()) {
file = Paths.get(System.getProperty("user.dir"), path).toFile()
- if(!file.exists()) {
+ if (!file.exists()) {
System.err.println("$parameter path is not valid, it doesn't exist (tried in \"$path\" and \"${file.absolutePath})\"")
exitProcess(1)
}
}
- if(shouldBeDirectory && !file.isDirectory) {
+ if (shouldBeDirectory && !file.isDirectory) {
System.err.println("$parameter path is not valid, the specified path is not a folder")
exitProcess(1)
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt
new file mode 100644
index 00000000..80131f9f
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/EOCVSimIconLibrary.kt
@@ -0,0 +1,28 @@
+package com.github.serivesmejia.eocvsim.gui
+
+object EOCVSimIconLibrary {
+
+ val icoEOCVSim by icon("ico_eocvsim", "/images/icon/ico_eocvsim.png", false)
+
+ val icoImg by icon("ico_img", "/images/icon/ico_img.png")
+ val icoCam by icon("ico_cam", "/images/icon/ico_cam.png")
+ val icoVid by icon("ico_vid", "/images/icon/ico_vid.png")
+
+ val icoConfig by icon("ico_config", "/images/icon/ico_config.png")
+ val icoSlider by icon("ico_slider", "/images/icon/ico_slider.png")
+ val icoTextbox by icon("ico_textbox", "/images/icon/ico_textbox.png")
+ val icoColorPick by icon("ico_colorpick", "/images/icon/ico_colorpick.png")
+
+ val icoArrowDropdown by icon("ico_arrow_dropdown", "/images/icon/ico_arrow_dropdown.png")
+
+ val icoFlag by icon("ico_flag", "/images/icon/ico_flag.png")
+ val icoNotStarted by icon("ico_not_started", "/images/icon/ico_not_started.png")
+ val icoPlay by icon("ico_play", "/images/icon/ico_play.png")
+ val icoStop by icon("ico_stop", "/images/icon/ico_stop.png")
+
+ val icoGears by icon("ico_gears", "/images/icon/ico_gears.png")
+ val icoHammer by icon("ico_hammer", "/images/icon/ico_hammer.png")
+
+ val icoColorPickPointer by icon("ico_colorpick_pointer", "/images/icon/ico_colorpick_pointer.png")
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt
new file mode 100644
index 00000000..b5d95097
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/IconLibrary.kt
@@ -0,0 +1,17 @@
+package com.github.serivesmejia.eocvsim.gui
+
+import java.awt.image.BufferedImage
+import javax.swing.ImageIcon
+import kotlin.reflect.KProperty
+
+fun icon(name: String, path: String, allowInvert: Boolean = true) = EOCVSimIconDelegate(name, path, allowInvert)
+
+class EOCVSimIconDelegate(val name: String, val path: String, allowInvert: Boolean = true) {
+ init {
+ Icons.addFutureImage(name, path, allowInvert)
+ }
+
+ operator fun getValue(eocvSimIconLibrary: EOCVSimIconLibrary, property: KProperty<*>): Icons.NamedImageIcon {
+ return Icons.getImage(name)
+ }
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt
index d603460d..76f864d8 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt
@@ -25,6 +25,7 @@ package com.github.serivesmejia.eocvsim.gui
import com.github.serivesmejia.eocvsim.gui.util.GuiUtil
import com.github.serivesmejia.eocvsim.util.loggerForThis
+import io.github.deltacv.vision.external.gui.util.ImgUtil
import java.awt.image.BufferedImage
import java.util.NoSuchElementException
import javax.swing.ImageIcon
@@ -33,8 +34,8 @@ object Icons {
private val bufferedImages = HashMap()
- private val icons = HashMap()
- private val resizedIcons = HashMap()
+ private val icons = HashMap()
+ private val resizedIcons = HashMap()
private val futureIcons = mutableListOf()
@@ -42,25 +43,7 @@ object Icons {
val logger by loggerForThis()
- init {
- addFutureImage("ico_eocvsim", "/images/icon/ico_eocvsim.png", false)
-
- addFutureImage("ico_img", "/images/icon/ico_img.png")
- addFutureImage("ico_cam", "/images/icon/ico_cam.png")
- addFutureImage("ico_vid", "/images/icon/ico_vid.png")
-
- addFutureImage("ico_config", "/images/icon/ico_config.png")
- addFutureImage("ico_slider", "/images/icon/ico_slider.png")
- addFutureImage("ico_textbox", "/images/icon/ico_textbox.png")
- addFutureImage("ico_colorpick", "/images/icon/ico_colorpick.png")
-
- addFutureImage("ico_gears", "/images/icon/ico_gears.png")
- addFutureImage("ico_hammer", "/images/icon/ico_hammer.png")
-
- addFutureImage("ico_colorpick_pointer", "/images/icon/ico_colorpick_pointer.png")
- }
-
- fun getImage(name: String): ImageIcon {
+ fun getImage(name: String): NamedImageIcon {
for(futureIcon in futureIcons.toTypedArray()) {
if(futureIcon.name == name) {
logger.trace("Loading future icon $name")
@@ -80,7 +63,7 @@ object Icons {
getImageResized(name, width, height)
}
- fun getImageResized(name: String, width: Int, height: Int): ImageIcon {
+ fun getImageResized(name: String, width: Int, height: Int): NamedImageIcon {
//determines the icon name from the:
//name, widthxheight, is inverted or is original
val resIconName = "$name-${width}x${height}${
@@ -94,7 +77,7 @@ object Icons {
val icon = if(resizedIcons.contains(resIconName)) {
resizedIcons[resIconName]
} else {
- resizedIcons[resIconName] = GuiUtil.scaleImage(getImage(name), width, height)
+ resizedIcons[resIconName] = NamedImageIcon(name, ImgUtil.scaleImage(getImage(name), width, height).image)
resizedIcons[resIconName]
}
@@ -112,7 +95,7 @@ object Icons {
}
bufferedImages[name] = Image(buffImg, allowInvert)
- icons[name] = ImageIcon(buffImg)
+ icons[name] = NamedImageIcon(name, buffImg)
}
fun setDark(dark: Boolean) {
@@ -141,4 +124,25 @@ object Icons {
data class FutureIcon(val name: String, val resourcePath: String, val allowInvert: Boolean)
+ class NamedImageIcon internal constructor(val name: String, image: java.awt.Image) : ImageIcon(image) {
+ fun resized(width: Int, height: Int) = getImageResized(name, width, height)
+
+ fun lazyResized(width: Int, height: Int) = lazyGetImageResized(name, width, height)
+
+ fun scaleToFit(suggestedWidth: Int, suggestedHeight: Int): NamedImageIcon {
+ val width = iconWidth
+ val height = iconHeight
+
+ return if (width > height) {
+ val newWidth = suggestedWidth
+ val newHeight = (height * newWidth) / width
+ resized(newWidth, newHeight)
+ } else {
+ val newHeight = suggestedHeight
+ val newWidth = (width * newHeight) / height
+ resized(newWidth, newHeight)
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java
index 12fc1dcd..323bed34 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java
@@ -26,21 +26,21 @@
import com.formdev.flatlaf.FlatLaf;
import com.github.serivesmejia.eocvsim.Build;
import com.github.serivesmejia.eocvsim.EOCVSim;
-import com.github.serivesmejia.eocvsim.gui.component.Viewport;
+import com.github.serivesmejia.eocvsim.gui.component.CollapsiblePanelX;
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.*;
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel;
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.SourceSelectorPanel;
+import io.github.deltacv.vision.external.gui.SwingOpenCvViewport;
import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker;
import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel;
-import com.github.serivesmejia.eocvsim.gui.component.visualizer.InputSourceDropTarget;
-import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel;
-import com.github.serivesmejia.eocvsim.gui.component.visualizer.TelemetryPanel;
-import com.github.serivesmejia.eocvsim.gui.component.visualizer.TopMenuBar;
import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel;
import com.github.serivesmejia.eocvsim.gui.theme.Theme;
-import com.github.serivesmejia.eocvsim.gui.util.ReflectTaskbar;
import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompiler;
import com.github.serivesmejia.eocvsim.util.event.EventHandler;
import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher;
import com.github.serivesmejia.eocvsim.workspace.util.template.GradleWorkspaceTemplate;
import kotlin.Unit;
+import org.opencv.core.Size;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -50,7 +50,6 @@
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
public class Visualizer {
@@ -64,20 +63,24 @@ public class Visualizer {
private final EOCVSim eocvSim;
public JFrame frame;
- public Viewport viewport = null;
- public TopMenuBar menuBar = null;
- public JPanel tunerMenuPanel = new JPanel();
+ public SwingOpenCvViewport viewport = null;
- public JScrollPane imgScrollPane = null;
+ public TopMenuBar menuBar = null;
+ public JPanel tunerMenuPanel;
public JPanel rightContainer = null;
- public JSplitPane globalSplitPane = null;
- public JSplitPane imageTunerSplitPane = null;
+
+ public PipelineOpModeSwitchablePanel pipelineOpModeSwitchablePanel = null;
public PipelineSelectorPanel pipelineSelectorPanel = null;
public SourceSelectorPanel sourceSelectorPanel = null;
+
+ public OpModeSelectorPanel opModeSelectorPanel = null;
+
public TelemetryPanel telemetryPanel;
+ public JPanel tunerCollapsible;
+
private String title = "EasyOpenCV Simulator v" + Build.standardVersionString;
private String titleMsg = "No pipeline";
private String beforeTitle = "";
@@ -85,9 +88,6 @@ public class Visualizer {
public ColorPicker colorPicker = null;
- //stuff for zooming handling
- private volatile boolean isCtrlPressed = false;
-
private volatile boolean hasFinishedInitializing = false;
Logger logger = LoggerFactory.getLogger(getClass());
@@ -97,10 +97,10 @@ public Visualizer(EOCVSim eocvSim) {
}
public void init(Theme theme) {
- if(ReflectTaskbar.INSTANCE.isUsable()){
+ if(Taskbar.isTaskbarSupported()){
try {
//set icon for mac os (and other systems which do support this method)
- ReflectTaskbar.INSTANCE.setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage());
+ Taskbar.getTaskbar().setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage());
} catch (final UnsupportedOperationException e) {
logger.warn("Setting the Taskbar icon image is not supported on this platform");
} catch (final SecurityException e) {
@@ -122,15 +122,33 @@ public void init(Theme theme) {
//instantiate all swing elements after theme installation
frame = new JFrame();
- viewport = new Viewport(eocvSim, eocvSim.getConfig().pipelineMaxFps.getFps());
+
+ String fpsMeterDescriptor = "deltacv EOCV-Sim v" + Build.standardVersionString;
+ if(Build.isDev) fpsMeterDescriptor += "-dev";
+
+ viewport = new SwingOpenCvViewport(new Size(1080, 720), fpsMeterDescriptor);
+ viewport.setDark(FlatLaf.isLafDark());
+
+ colorPicker = new ColorPicker(viewport);
+
+ JLayeredPane skiaPanel = viewport.skiaPanel();
+ skiaPanel.setLayout(new BorderLayout());
+
+ frame.add(skiaPanel);
menuBar = new TopMenuBar(this, eocvSim);
tunerMenuPanel = new JPanel();
- pipelineSelectorPanel = new PipelineSelectorPanel(eocvSim);
- sourceSelectorPanel = new SourceSelectorPanel(eocvSim);
- telemetryPanel = new TelemetryPanel();
+ pipelineOpModeSwitchablePanel = new PipelineOpModeSwitchablePanel(eocvSim);
+ pipelineOpModeSwitchablePanel.disableSwitching();
+
+ pipelineSelectorPanel = pipelineOpModeSwitchablePanel.getPipelineSelectorPanel();
+ sourceSelectorPanel = pipelineOpModeSwitchablePanel.getSourceSelectorPanel();
+
+ opModeSelectorPanel = pipelineOpModeSwitchablePanel.getOpModeSelectorPanel();
+
+ telemetryPanel = new TelemetryPanel();
rightContainer = new JPanel();
@@ -144,55 +162,42 @@ public void init(Theme theme) {
* IMG VISUALIZER & SCROLL PANE
*/
- imgScrollPane = new JScrollPane(viewport);
-
- imgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
- imgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
-
- imgScrollPane.getHorizontalScrollBar().setUnitIncrement(16);
- imgScrollPane.getVerticalScrollBar().setUnitIncrement(16);
-
rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS));
+ // add pretty border
+ rightContainer.setBorder(
+ BorderFactory.createMatteBorder(0, 1, 0, 0, UIManager.getColor("Separator.foreground"))
+ );
- /*
- * PIPELINE SELECTOR
- */
- pipelineSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20));
- rightContainer.add(pipelineSelectorPanel);
-
- /*
- * SOURCE SELECTOR
- */
- sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20));
- rightContainer.add(sourceSelectorPanel);
+ pipelineOpModeSwitchablePanel.setBorder(new EmptyBorder(0, 0, 0, 0));
+ rightContainer.add(pipelineOpModeSwitchablePanel);
/*
* TELEMETRY
*/
- telemetryPanel.setBorder(new EmptyBorder(0, 20, 20, 20));
- rightContainer.add(telemetryPanel);
- /*
- * SPLIT
- */
+ JPanel telemetryWithInsets = new JPanel();
+ telemetryWithInsets.setLayout(new BoxLayout(telemetryWithInsets, BoxLayout.LINE_AXIS));
+ telemetryWithInsets.setBorder(new EmptyBorder(0, 20, 20, 20));
- //left side, image scroll & tuner menu split panel
- imageTunerSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, imgScrollPane, tunerMenuPanel);
+ telemetryWithInsets.add(telemetryPanel);
- imageTunerSplitPane.setResizeWeight(1);
- imageTunerSplitPane.setOneTouchExpandable(false);
- imageTunerSplitPane.setContinuousLayout(true);
+ rightContainer.add(telemetryWithInsets);
//global
- globalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, imageTunerSplitPane, rightContainer);
+ frame.getContentPane().setDropTarget(new InputSourceDropTarget(eocvSim));
+
+ tunerCollapsible = new CollapsiblePanelX("Variable Tuner", null, null);
+ tunerCollapsible.setLayout(new BoxLayout(tunerCollapsible, BoxLayout.LINE_AXIS));
+ tunerCollapsible.setVisible(false);
- globalSplitPane.setResizeWeight(1);
- globalSplitPane.setOneTouchExpandable(false);
- globalSplitPane.setContinuousLayout(true);
+ JScrollPane tunerScrollPane = new JScrollPane(tunerMenuPanel);
+ tunerScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+ tunerScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
- globalSplitPane.setDropTarget(new InputSourceDropTarget(eocvSim));
+ tunerCollapsible.add(tunerScrollPane);
- frame.add(globalSplitPane, BorderLayout.CENTER);
+ frame.add(tunerCollapsible, BorderLayout.SOUTH);
+ frame.add(rightContainer, BorderLayout.EAST);
//initialize other various stuff of the frame
frame.setSize(780, 645);
@@ -207,10 +212,6 @@ public void init(Theme theme) {
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- globalSplitPane.setDividerLocation(1070);
-
- colorPicker = new ColorPicker(viewport.image);
-
frame.setVisible(true);
onInitFinished.run();
@@ -238,7 +239,7 @@ public void windowClosing(WindowEvent e) {
});
//handling onViewportTapped evts
- viewport.addMouseListener(new MouseAdapter() {
+ viewport.getComponent().addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if(!colorPicker.isPicking())
eocvSim.pipelineManager.callViewportTapped();
@@ -246,63 +247,21 @@ public void mouseClicked(MouseEvent e) {
});
//VIEWPORT RESIZE HANDLING
- imgScrollPane.addMouseWheelListener(e -> {
- if (isCtrlPressed) { //check if control key is pressed
- double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation());
- viewport.setViewportScale(scale);
- }
- });
-
- //listening for keyboard presses and releases, to check if ctrl key was pressed or released (handling zoom)
- KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ke -> {
- switch (ke.getID()) {
- case KeyEvent.KEY_PRESSED:
- if (ke.getKeyCode() == KeyEvent.VK_CONTROL) {
- isCtrlPressed = true;
- imgScrollPane.setWheelScrollingEnabled(false); //lock scrolling if ctrl is pressed
- }
- break;
- case KeyEvent.KEY_RELEASED:
- if (ke.getKeyCode() == KeyEvent.VK_CONTROL) {
- isCtrlPressed = false;
- imgScrollPane.setWheelScrollingEnabled(true); //unlock
- }
- break;
- }
- return false; //idk let's just return false 'cause keyboard input doesn't work otherwise
- });
-
- //resizes all three JLists in right panel to make buttons visible in smaller resolutions
- frame.addComponentListener(new ComponentAdapter() {
- @Override
- public void componentResized(ComponentEvent evt) {
- double ratioH = frame.getSize().getHeight() / 645;
-
- double fontSize = 17 * ratioH;
- Font font = pipelineSelectorPanel.getPipelineSelectorLabel().getFont().deriveFont((float)fontSize);
-
- pipelineSelectorPanel.getPipelineSelectorLabel().setFont(font);
- pipelineSelectorPanel.revalAndRepaint();
-
- sourceSelectorPanel.getSourceSelectorLabel().setFont(font);
- sourceSelectorPanel.revalAndRepaint();
-
- telemetryPanel.getTelemetryLabel().setFont(font);
- telemetryPanel.revalAndRepaint();
-
- rightContainer.revalidate();
- rightContainer.repaint();
- }
- });
+ // imgScrollPane.addMouseWheelListener(e -> {
+ // if (isCtrlPressed) { //check if control key is pressed
+ // double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation());
+ // viewport.setViewportScale(scale);
+ // }
+ // });
// stop color-picking mode when changing pipeline
// TODO: find out why this breaks everything?????
- // eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking());
+ eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking());
}
public boolean hasFinishedInit() { return hasFinishedInitializing; }
- public void waitForFinishingInit() {
+ public void joinInit() {
while (!hasFinishedInitializing) {
Thread.yield();
}
@@ -311,7 +270,7 @@ public void waitForFinishingInit() {
public void close() {
SwingUtilities.invokeLater(() -> {
frame.setVisible(false);
- viewport.stop();
+ viewport.deactivate();
//close all asyncpleasewait dialogs
for (AsyncPleaseWaitDialog dialog : pleaseWaitDialogs) {
@@ -342,7 +301,7 @@ public void close() {
childDialogs.clear();
frame.dispose();
- viewport.flush();
+ viewport.deactivate();
});
}
@@ -370,6 +329,8 @@ public void updateTunerFields(List fields) {
tunerMenuPanel.add(fieldPanel);
fieldPanel.showFieldPanel();
}
+
+ tunerCollapsible.setVisible(!fields.isEmpty());
}
public void asyncCompilePipelines() {
@@ -390,7 +351,7 @@ public void asyncCompilePipelines() {
public void compilingUnsupported() {
asyncPleaseWaitDialog(
- "Runtime compiling is not supported on this JVM",
+ "Runtime pipeline builds are not supported on this JVM",
"For further info, check the EOCV-Sim GitHub repo",
"Close",
new Dimension(320, 160),
@@ -438,7 +399,7 @@ public void createVSCodeWorkspace() {
}
public void askOpenVSCode() {
- DialogFactory.createYesOrNo(frame, "A new workspace was created. Do you wanna open VS Code?", "",
+ DialogFactory.createYesOrNo(frame, "A new workspace was created. Do you want to open VS Code?", "",
(result) -> {
if(result == 0) {
VSCodeLauncher.INSTANCE.asyncLaunch(eocvSim.workspaceManager.getWorkspaceFile());
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt
new file mode 100644
index 00000000..0822043a
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/CollapsiblePanelX.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ * Credit where it's due - based off of https://stackoverflow.com/a/52956783
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.gui.component
+
+import java.awt.Color
+import java.awt.Dimension
+import java.awt.event.MouseAdapter
+import java.awt.event.MouseEvent
+import javax.swing.BorderFactory
+import javax.swing.JLabel
+import javax.swing.JPanel
+import javax.swing.border.LineBorder
+import javax.swing.border.TitledBorder
+
+class CollapsiblePanelX @JvmOverloads constructor(
+ title: String?,
+ titleCol: Color?,
+ borderCol: Color? = Color.white
+) : JPanel() {
+ private val border: TitledBorder
+ private var collapsible = true
+
+ var isHidden = false
+ private set
+
+ init {
+ val titleAndDescriptor = if(isHidden) {
+ "$title (click here to expand)"
+ } else {
+ "$title (click here to hide)"
+ }
+
+ border = TitledBorder(titleAndDescriptor)
+
+ border.titleColor = titleCol
+ border.border = BorderFactory.createMatteBorder(1, 1, 1, 1, borderCol)
+
+ setBorder(border)
+
+ // as Titleborder has no access to the Label we fake the size data ;)
+ val l = JLabel(titleAndDescriptor)
+ val size = l.getPreferredSize()
+
+ addMouseListener(object : MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent) {
+ if (!collapsible) {
+ return
+ }
+
+ val i = getBorder().getBorderInsets(this@CollapsiblePanelX)
+ if (e.x < i.left + size.width && e.y < i.bottom + size.height) {
+
+ for(e in components) {
+ e.isVisible = !isHidden
+
+ border.title = if(isHidden) {
+ "$title (click here to expand)"
+ } else {
+ "$title (click here to hide)"
+ }
+
+ isHidden = !isHidden
+ }
+
+ revalidate()
+ e.consume()
+ }
+ }
+ })
+ }
+
+ fun setCollapsible(collapsible: Boolean) {
+ this.collapsible = collapsible
+ }
+
+ fun setTitle(title: String?) {
+ border.title = title
+ }
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt
index 15cf5bd3..d9d7e590 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt
@@ -23,16 +23,15 @@
package com.github.serivesmejia.eocvsim.gui.component
+import com.github.serivesmejia.eocvsim.gui.util.Corner
import com.github.serivesmejia.eocvsim.util.event.EventHandler
+import java.awt.Point
import java.awt.Window
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.WindowEvent
import java.awt.event.WindowFocusListener
-import javax.swing.JPanel
-import javax.swing.JPopupMenu
-import javax.swing.JWindow
-import javax.swing.Popup
+import javax.swing.*
class PopupX @JvmOverloads constructor(windowAncestor: Window,
private val panel: JPanel,
@@ -97,4 +96,55 @@ class PopupX @JvmOverloads constructor(windowAncestor: Window,
fun setLocation(width: Int, height: Int) = window.setLocation(width, height)
+ companion object {
+
+ fun JComponent.popUpXOnThis(
+ panel: JPanel,
+ buttonCorner: Corner = Corner.TOP_LEFT,
+ popupCorner: Corner = Corner.BOTTOM_LEFT,
+ closeOnFocusLost: Boolean = true
+ ): PopupX {
+
+ val frame = SwingUtilities.getWindowAncestor(this)
+ val location = locationOnScreen
+
+ val cornerLocation: Point = when(buttonCorner) {
+ Corner.TOP_LEFT -> Point(location.x, location.y)
+ Corner.TOP_RIGHT -> Point(location.x + width, location.y)
+ Corner.BOTTOM_LEFT -> Point(location.x, location.y + height)
+ Corner.BOTTOM_RIGHT -> Point(location.x + width, location.y + height)
+ }
+
+ val popup = PopupX(frame, panel,
+ cornerLocation.x,
+ cornerLocation.y,
+ closeOnFocusLost
+ )
+
+ popup.onShow {
+ when(popupCorner) {
+ Corner.TOP_LEFT -> popup.setLocation(
+ popup.window.location.x,
+ popup.window.location.y + popup.window.height
+ )
+ Corner.TOP_RIGHT -> popup.setLocation(
+ popup.window.location.x - popup.window.width,
+ popup.window.location.y + popup.window.height
+ )
+ Corner.BOTTOM_LEFT -> popup.setLocation(
+ popup.window.location.x + width,
+ popup.window.location.y + popup.window.height
+ )
+ Corner.BOTTOM_RIGHT -> popup.setLocation(
+ popup.window.location.x - popup.window.width,
+ popup.window.location.y + popup.window.height
+ )
+ }
+ }
+
+ return popup
+ }
+
+ }
+
}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java
deleted file mode 100644
index 3eceb84e..00000000
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.gui.component;
-
-import com.github.serivesmejia.eocvsim.EOCVSim;
-import com.github.serivesmejia.eocvsim.gui.util.MatPoster;
-import com.github.serivesmejia.eocvsim.util.image.DynamicBufferedImageRecycler;
-import com.github.serivesmejia.eocvsim.util.CvUtil;
-import com.qualcomm.robotcore.util.Range;
-import org.opencv.core.Mat;
-import org.opencv.core.Size;
-import org.opencv.imgproc.Imgproc;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-
-public class Viewport extends JPanel {
-
- public final ImageX image = new ImageX();
- public final MatPoster matPoster;
-
- private Mat lastVisualizedMat = null;
- private Mat lastVisualizedScaledMat = null;
-
- private final DynamicBufferedImageRecycler buffImgGiver = new DynamicBufferedImageRecycler();
-
- private volatile BufferedImage lastBuffImage;
- private volatile Dimension lastDimension;
-
- private double scale;
-
- private final EOCVSim eocvSim;
-
- Logger logger = LoggerFactory.getLogger(getClass());
-
- public Viewport(EOCVSim eocvSim, int maxQueueItems) {
- super(new GridBagLayout());
-
- this.eocvSim = eocvSim;
- setViewportScale(eocvSim.configManager.getConfig().zoom);
-
- add(image, new GridBagConstraints());
-
- matPoster = new MatPoster("Viewport", maxQueueItems);
- attachToPoster(matPoster);
- }
-
- public void postMatAsync(Mat mat) {
- matPoster.post(mat);
- }
-
- public synchronized void postMat(Mat mat) {
- if(lastVisualizedMat == null) lastVisualizedMat = new Mat(); //create latest mat if we have null reference
- if(lastVisualizedScaledMat == null) lastVisualizedScaledMat = new Mat(); //create last scaled mat if null reference
-
- JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this);
-
- mat.copyTo(lastVisualizedMat); //copy given mat to viewport latest one
-
- double wScale = (double) frame.getWidth() / mat.width();
- double hScale = (double) frame.getHeight() / mat.height();
-
- double calcScale = (wScale / hScale) * 1.5;
- double finalScale = Math.max(0.1, Math.min(3, scale * calcScale));
-
- Size size = new Size(mat.width() * finalScale, mat.height() * finalScale);
- Imgproc.resize(mat, lastVisualizedScaledMat, size, 0.0, 0.0, Imgproc.INTER_AREA); //resize mat to lastVisualizedScaledMat
-
- Dimension newDimension = new Dimension(lastVisualizedScaledMat.width(), lastVisualizedScaledMat.height());
-
- if(lastBuffImage != null) buffImgGiver.returnBufferedImage(lastBuffImage);
-
- lastBuffImage = buffImgGiver.giveBufferedImage(newDimension, 2);
- lastDimension = newDimension;
-
- CvUtil.matToBufferedImage(lastVisualizedScaledMat, lastBuffImage);
-
- image.setImage(lastBuffImage); //set buff image to ImageX component
-
- eocvSim.configManager.getConfig().zoom = scale; //store latest scale if store setting turned on
- }
-
- public void attachToPoster(MatPoster poster) {
- poster.addPostable((m) -> {
- try {
- Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2BGR);
- postMat(m);
- } catch(Exception ex) {
- logger.error("Couldn't visualize last mat", ex);
- }
- });
- }
-
- public void flush() {
- buffImgGiver.flushAll();
- }
-
- public void stop() {
- matPoster.stop();
- flush();
- }
-
- public synchronized void setViewportScale(double scale) {
- scale = Range.clip(scale, 0.1, 3);
-
- boolean scaleChanged = this.scale != scale;
- this.scale = scale;
-
- if(lastVisualizedMat != null && scaleChanged)
- postMat(lastVisualizedMat);
-
- }
-
- public synchronized Mat getLastVisualizedMat() {
- return lastVisualizedMat;
- }
-
- public synchronized double getViewportScale() {
- return scale;
- }
-
-}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt
index 3244f25a..7826c415 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt
@@ -23,27 +23,27 @@
package com.github.serivesmejia.eocvsim.gui.component.tuner
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary
import com.github.serivesmejia.eocvsim.util.SysUtil
-import com.github.serivesmejia.eocvsim.gui.Icons
-import com.github.serivesmejia.eocvsim.gui.component.ImageX
-import com.github.serivesmejia.eocvsim.gui.component.Viewport
import com.github.serivesmejia.eocvsim.util.event.EventHandler
+import io.github.deltacv.vision.external.gui.SwingOpenCvViewport
import org.opencv.core.Scalar
-import java.awt.Color
import java.awt.Cursor
import java.awt.Point
+import java.awt.Robot
+import java.awt.Toolkit
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
-import java.awt.Toolkit
-class ColorPicker(private val imageX: ImageX) {
+
+class ColorPicker(private val viewport: SwingOpenCvViewport) {
companion object {
private val size = if(SysUtil.OS == SysUtil.OperatingSystem.WINDOWS) {
200
} else { 35 }
- val colorPickIco = Icons.getImageResized("ico_colorpick_pointer", size, size).image
+ val colorPickIco = EOCVSimIconLibrary.icoColorPickPointer.resized(size, size).image
val colorPickCursor = Toolkit.getDefaultToolkit().createCustomCursor(
colorPickIco, Point(0, 0), "Color Pick Pointer"
@@ -68,10 +68,8 @@ class ColorPicker(private val imageX: ImageX) {
override fun mouseClicked(e: MouseEvent) {
//if clicked with primary button...
if(e.button == MouseEvent.BUTTON1) {
- //get the "packed" (in a single int value) color from the image at mouse position's pixel
- val packedColor = imageX.image.getRGB(e.x, e.y)
- //parse the "packed" color into four separate channels
- val color = Color(packedColor, true)
+ // The pixel color at location x, y
+ val color = Robot().getPixelColor(e.xOnScreen, e.yOnScreen)
//wrap Java's color to OpenCV's Scalar since we're EOCV-Sim not JavaCv-Sim right?
colorRgb = Scalar(
@@ -93,10 +91,10 @@ class ColorPicker(private val imageX: ImageX) {
isPicking = true
hasPicked = false
- imageX.addMouseListener(clickListener)
+ viewport.component.addMouseListener(clickListener)
- initialCursor = imageX.cursor
- imageX.cursor = colorPickCursor
+ initialCursor = viewport.component.cursor
+ viewport.component.cursor = colorPickCursor
}
fun stopPicking() {
@@ -108,8 +106,8 @@ class ColorPicker(private val imageX: ImageX) {
onCancel.run()
}
- imageX.removeMouseListener(clickListener)
- imageX.cursor = initialCursor
+ viewport.component.removeMouseListener(clickListener)
+ viewport.component.cursor = initialCursor
}
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt
index 3b4b37a2..4c1cc071 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt
@@ -57,10 +57,10 @@ class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions
private val sliderRangeFieldsPanel = JPanel()
private var sliderRangeFields = createRangeFields()
- private val colorSpaceComboBox = EnumComboBox("Color space: ", PickerColorSpace::class.java, PickerColorSpace.values())
+ private val colorSpaceComboBox = EnumComboBox("Color space: ", PickerColorSpace::class.java, PickerColorSpace.entries.toTypedArray())
private val applyToAllButtonPanel = JPanel(GridBagLayout())
- private val applyToAllButton = JToggleButton("Apply to all fields...")
+ private val applyToAllButton = JToggleButton("Apply to all variables...")
private val applyModesPanel = JPanel()
private val applyToAllGloballyButton = JButton("Globally")
@@ -89,7 +89,7 @@ class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions
LOCAL("From local config"),
GLOBAL("From global config"),
GLOBAL_DEFAULT("From default global config"),
- TYPE_SPECIFIC("From specific config")
+ TYPE_SPECIFIC("From type config")
}
data class Config(var sliderRange: Size,
@@ -294,6 +294,10 @@ class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions
}
configSourceLabel.text = localConfig.source.description
+
+ if(currentConfig.source == ConfigSource.LOCAL || currentConfig.source == ConfigSource.TYPE_SPECIFIC) {
+ configSourceLabel.text += ": ${fieldOptions.fieldPanel.tunableField.fieldTypeName}"
+ }
}
//updates the actual configuration displayed on the field panel gui
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt
index 9be6461a..b7d66e08 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt
@@ -24,9 +24,10 @@
package com.github.serivesmejia.eocvsim.gui.component.tuner
import com.github.serivesmejia.eocvsim.EOCVSim
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary
import com.github.serivesmejia.eocvsim.gui.Icons
import com.github.serivesmejia.eocvsim.gui.component.PopupX
-import com.github.serivesmejia.eocvsim.util.extension.cvtColor
+import io.github.deltacv.vision.external.util.extension.cvtColor
import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero
import java.awt.FlowLayout
import java.awt.GridLayout
@@ -39,10 +40,10 @@ import javax.swing.event.AncestorListener
class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel,
eocvSim: EOCVSim) : JPanel() {
- private val sliderIco by Icons.lazyGetImageResized("ico_slider", 15, 15)
- private val textBoxIco by Icons.lazyGetImageResized("ico_textbox", 15, 15)
- private val configIco by Icons.lazyGetImageResized("ico_config", 15, 15)
- private val colorPickIco by Icons.lazyGetImageResized("ico_colorpick", 15, 15)
+ private val sliderIco by EOCVSimIconLibrary.icoSlider.lazyResized(15, 15)
+ private val textBoxIco by EOCVSimIconLibrary.icoTextbox.lazyResized(15, 15)
+ private val configIco by EOCVSimIconLibrary.icoConfig.lazyResized(15, 15)
+ private val colorPickIco by EOCVSimIconLibrary.icoColorPick.lazyResized(15, 15)
private val textBoxSliderToggle = JToggleButton()
private val configButton = JButton()
@@ -121,8 +122,8 @@ class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel,
startPicking(colorPicker)
} else { //handles cases when cancelling picking
colorPicker.stopPicking()
- //if we weren't the ones controlling the last picking,
- //start picking again to gain control for this panel
+ // if we weren't the ones controlling the last picking,
+ // start picking again to gain control for this panel
if(colorPickButton.isSelected) startPicking(colorPicker)
}
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt
index 5a73961c..48cc57e4 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt
@@ -37,13 +37,13 @@ import javax.swing.JPanel
class CreateSourcePanel(eocvSim: EOCVSim) : JPanel(GridLayout(2, 1)) {
private val sourceSelectComboBox = EnumComboBox(
- "", SourceType::class.java, SourceType.values(),
- { it.coolName }, { SourceType.fromCoolName(it) }
+ "", SourceType::class.java, SourceType.values(),
+ { it.coolName }, { SourceType.fromCoolName(it) }
)
private val cameraDriverComboBox = EnumComboBox(
- "Camera driver: ", WebcamDriver::class.java, WebcamDriver.values(),
- { it.name.replace("_", " ") }, { WebcamDriver.valueOf(it.replace(" ", "_")) }
+ "Camera driver: ", WebcamDriver::class.java, WebcamDriver.values(),
+ { it.name.replace("_", " ") }, { WebcamDriver.valueOf(it.replace(" ", "_")) }
)
@@ -69,4 +69,4 @@ class CreateSourcePanel(eocvSim: EOCVSim) : JPanel(GridLayout(2, 1)) {
add(nextPanel)
}
-}
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt
new file mode 100644
index 00000000..7b8c21af
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/PipelineOpModeSwitchablePanel.kt
@@ -0,0 +1,132 @@
+package com.github.serivesmejia.eocvsim.gui.component.visualizer
+
+import com.github.serivesmejia.eocvsim.EOCVSim
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeControlsPanel
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode.OpModeSelectorPanel
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.SourceSelectorPanel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.swing.Swing
+import java.awt.GridBagConstraints
+import java.awt.GridBagLayout
+import java.awt.GridLayout
+import java.awt.Insets
+import javax.swing.BoxLayout
+import javax.swing.JPanel
+import javax.swing.JTabbedPane
+import javax.swing.border.EmptyBorder
+import javax.swing.border.TitledBorder
+
+class PipelineOpModeSwitchablePanel(val eocvSim: EOCVSim) : JTabbedPane() {
+
+ val pipelinePanel = JPanel()
+
+ val pipelineSelectorPanel = PipelineSelectorPanel(eocvSim)
+ val sourceSelectorPanel = SourceSelectorPanel(eocvSim)
+
+ val opModePanel = JPanel()
+
+ val opModeControlsPanel = OpModeControlsPanel(eocvSim)
+ val opModeSelectorPanel = OpModeSelectorPanel(eocvSim, opModeControlsPanel)
+
+ init {
+ pipelinePanel.layout = GridBagLayout()
+
+ pipelineSelectorPanel.border = TitledBorder("Pipelines").apply {
+ border = EmptyBorder(0, 0, 0, 0)
+ }
+
+ pipelinePanel.add(pipelineSelectorPanel, GridBagConstraints().apply {
+ gridx = 0
+ gridy = 0
+
+ weightx = 1.0
+ weighty = 1.0
+ fill = GridBagConstraints.BOTH
+
+ insets = Insets(10, 20, 5, 20)
+ })
+
+ sourceSelectorPanel.border = TitledBorder("Sources").apply {
+ border = EmptyBorder(0, 0, 0, 0)
+ }
+
+ pipelinePanel.add(sourceSelectorPanel, GridBagConstraints().apply {
+ gridx = 0
+ gridy = 1
+
+ weightx = 1.0
+ weighty = 1.0
+ fill = GridBagConstraints.BOTH
+
+ insets = Insets(-5, 20, -10, 20)
+ })
+
+ opModePanel.layout = GridLayout(2, 1)
+
+ opModeSelectorPanel.border = EmptyBorder(0, 20, 20, 20)
+ opModePanel.add(opModeSelectorPanel)
+
+ opModeControlsPanel.border = EmptyBorder(0, 20, 20, 20)
+ opModePanel.add(opModeControlsPanel)
+
+ add("Pipeline", JPanel().apply {
+ layout = BoxLayout(this, BoxLayout.LINE_AXIS)
+ add(pipelinePanel)
+ })
+ add("OpMode", opModePanel)
+
+ addChangeListener {
+ val sourceTabbedPane = it.source as JTabbedPane
+ val index = sourceTabbedPane.selectedIndex
+
+ if(index == 0) {
+ pipelineSelectorPanel.isActive = true
+ opModeSelectorPanel.isActive = false
+
+ opModeSelectorPanel.reset(0)
+ } else if(index == 1) {
+ opModeSelectorPanel.reset()
+
+ pipelineSelectorPanel.isActive = false
+ opModeSelectorPanel.isActive = true
+ }
+ }
+ }
+
+ fun updateSelectorLists() {
+ pipelineSelectorPanel.updatePipelinesList()
+ opModeSelectorPanel.updateOpModesList()
+ }
+
+ fun updateSelectorListsBlocking() = runBlocking {
+ launch(Dispatchers.Swing) {
+ updateSelectorLists()
+ }
+ }
+
+ fun enableSwitching() {
+ pipelineSelectorPanel.allowPipelineSwitching = true
+ opModeSelectorPanel.isActive = true
+ }
+
+ fun disableSwitching() {
+ pipelineSelectorPanel.allowPipelineSwitching = false
+ opModeSelectorPanel.isActive = false
+ }
+
+ fun enableSwitchingBlocking() = runBlocking {
+ launch(Dispatchers.Swing) {
+ enableSwitching()
+ }
+ }
+
+ fun disableSwitchingBlocking() = runBlocking {
+ launch(Dispatchers.Swing) {
+ disableSwitching()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt
index e489f25e..7cc470a1 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt
@@ -9,6 +9,8 @@ import java.awt.GridLayout
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionListener
import javax.swing.*
+import javax.swing.border.EmptyBorder
+import javax.swing.border.TitledBorder
class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver {
@@ -18,6 +20,10 @@ class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver {
val telemetryLabel = JLabel("Telemetry")
init {
+ border = TitledBorder("Telemetry").apply {
+ border = EmptyBorder(0, 0, 0, 0)
+ }
+
layout = GridBagLayout()
/*
@@ -27,10 +33,10 @@ class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver {
telemetryLabel.font = telemetryLabel.font.deriveFont(20.0f)
telemetryLabel.horizontalAlignment = JLabel.CENTER
- add(telemetryLabel, GridBagConstraints().apply {
- gridy = 0
- ipady = 20
- })
+ // add(telemetryLabel, GridBagConstraints().apply {
+ // gridy = 0
+ // ipady = 20
+ //})
telemetryScroll.setViewportView(telemetryList)
telemetryScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS
@@ -52,7 +58,7 @@ class TelemetryPanel : JPanel(), TelemetryTransmissionReceiver {
telemetryList.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
add(telemetryScroll, GridBagConstraints().apply {
- gridy = 1
+ gridy = 0
weightx = 0.5
weighty = 1.0
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt
index 0956403c..46397134 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt
@@ -33,6 +33,8 @@ import com.github.serivesmejia.eocvsim.input.SourceType
import com.github.serivesmejia.eocvsim.util.FileFilters
import com.github.serivesmejia.eocvsim.util.exception.handling.CrashReport
import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher
+import org.opencv.core.Mat
+import org.opencv.imgproc.Imgproc
import java.awt.Desktop
import java.io.File
import java.net.URI
@@ -49,7 +51,6 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() {
@JvmField val mFileMenu = JMenu("File")
@JvmField val mWorkspMenu = JMenu("Workspace")
- @JvmField val mEditMenu = JMenu("Edit")
@JvmField val mHelpMenu = JMenu("Help")
@JvmField val workspCompile = JMenuItem("Build java files")
@@ -76,19 +77,31 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() {
fileNewInputSourceSubmenu.add(fileNewInputSourceItem)
}
- val fileSaveMat = JMenuItem("Save current image")
+ val fileSaveMat = JMenuItem("Screenshot pipeline")
+
fileSaveMat.addActionListener {
+ val mat = Mat()
+ visualizer.viewport.pollLastFrame(mat)
+ Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGB2BGR)
+
GuiUtil.saveMatFileChooser(
visualizer.frame,
- visualizer.viewport.lastVisualizedMat,
+ mat,
eocvSim
)
+
+ mat.release()
}
mFileMenu.add(fileSaveMat)
mFileMenu.addSeparator()
+ val editSettings = JMenuItem("Settings")
+ editSettings.addActionListener { DialogFactory.createConfigDialog(eocvSim) }
+
+ mFileMenu.add(editSettings)
+
val fileRestart = JMenuItem("Restart")
fileRestart.addActionListener { eocvSim.onMainUpdate.doOnce { eocvSim.restart() } }
@@ -136,14 +149,6 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() {
add(mWorkspMenu)
- // EDIT
-
- val editSettings = JMenuItem("Settings")
- editSettings.addActionListener { DialogFactory.createConfigDialog(eocvSim) }
-
- mEditMenu.add(editSettings)
- add(mEditMenu)
-
// HELP
val helpUsage = JMenuItem("Documentation")
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt
new file mode 100644
index 00000000..5c01cedc
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeControlsPanel.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode
+
+import com.github.serivesmejia.eocvsim.EOCVSim
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary
+import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
+import com.qualcomm.robotcore.eventloop.opmode.OpMode
+import io.github.deltacv.vision.internal.opmode.OpModeNotification
+import io.github.deltacv.vision.internal.opmode.OpModeState
+import java.awt.BorderLayout
+import javax.swing.JPanel
+import javax.swing.JButton
+import javax.swing.SwingUtilities
+
+class OpModeControlsPanel(val eocvSim: EOCVSim) : JPanel() {
+
+ val controlButton = JButton()
+
+ var currentOpMode: OpMode? = null
+ private set
+
+ private var currentManagerIndex: Int? = null
+ private var upcomingIndex: Int? = null
+
+ var isActive = false
+
+ init {
+ layout = BorderLayout()
+
+ add(controlButton, BorderLayout.CENTER)
+
+ controlButton.isEnabled = false
+ controlButton.icon = EOCVSimIconLibrary.icoFlag
+
+ controlButton.addActionListener {
+ eocvSim.pipelineManager.onUpdate.doOnce {
+ if(eocvSim.pipelineManager.currentPipeline !is OpMode) return@doOnce
+
+ eocvSim.pipelineManager.setPaused(false, PipelineManager.PauseReason.NOT_PAUSED)
+
+ val opMode = eocvSim.pipelineManager.currentPipeline as OpMode
+ val state = opMode.notifier.state
+
+ opMode.notifier.notify(when(state) {
+ OpModeState.SELECTED -> OpModeNotification.INIT
+ OpModeState.INIT -> OpModeNotification.START
+ OpModeState.START -> OpModeNotification.STOP
+ else -> OpModeNotification.NOTHING
+ })
+ }
+ }
+ }
+
+ fun stopCurrentOpMode() {
+ if(eocvSim.pipelineManager.currentPipeline != currentOpMode || currentOpMode == null) return
+ currentOpMode!!.notifier.notify(OpModeNotification.STOP)
+ }
+
+ private fun notifySelected() {
+ if(!isActive) return
+
+ if(eocvSim.pipelineManager.currentPipeline !is OpMode) return
+ val opMode = eocvSim.pipelineManager.currentPipeline as OpMode
+ val opModeIndex = currentManagerIndex!!
+
+ opMode.notifier.onStateChange {
+ val state = opMode.notifier.state
+
+ SwingUtilities.invokeLater {
+ updateButtonState(state)
+ }
+
+ if(state == OpModeState.STOPPED) {
+ if(isActive && opModeIndex == upcomingIndex) {
+ opModeSelected(currentManagerIndex!!)
+ }
+
+ it.removeThis()
+ }
+ }
+
+ opMode.notifier.notify(OpModeState.SELECTED)
+
+ currentOpMode = opMode
+ }
+
+ private fun updateButtonState(state: OpModeState) {
+ when(state) {
+ OpModeState.SELECTED -> controlButton.isEnabled = true
+ OpModeState.INIT -> controlButton.icon = EOCVSimIconLibrary.icoPlay
+ OpModeState.START -> controlButton.icon = EOCVSimIconLibrary.icoStop
+ OpModeState.STOP -> {
+ controlButton.isEnabled = false
+ }
+ OpModeState.STOPPED -> {
+ controlButton.isEnabled = true
+
+ controlButton.icon = EOCVSimIconLibrary.icoFlag
+ }
+ }
+ }
+
+ fun opModeSelected(managerIndex: Int, forceChangePipeline: Boolean = true) {
+ eocvSim.pipelineManager.requestSetPaused(false)
+
+ if(forceChangePipeline) {
+ eocvSim.pipelineManager.requestForceChangePipeline(managerIndex)
+ }
+
+ upcomingIndex = managerIndex
+
+ eocvSim.pipelineManager.onUpdate.doOnce {
+ currentManagerIndex = managerIndex
+ notifySelected()
+ }
+ }
+
+ fun reset() {
+ controlButton.isEnabled = false
+ controlButton.icon = EOCVSimIconLibrary.icoFlag
+
+ currentOpMode?.requestOpModeStop()
+ }
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt
new file mode 100644
index 00000000..7c5637a2
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModePopupPanel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode
+
+import javax.swing.JList
+import javax.swing.JPanel
+import javax.swing.JScrollPane
+
+class OpModePopupPanel(autonomousSelector: JList<*>) : JPanel() {
+
+ init {
+ val scroll = JScrollPane()
+
+ scroll.setViewportView(autonomousSelector)
+ scroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS
+ scroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
+
+ add(scroll)
+
+ autonomousSelector.selectionModel.clearSelection()
+ }
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt
new file mode 100644
index 00000000..c6c041a5
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/opmode/OpModeSelectorPanel.kt
@@ -0,0 +1,338 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.gui.component.visualizer.opmode
+
+import com.github.serivesmejia.eocvsim.EOCVSim
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary
+import com.github.serivesmejia.eocvsim.gui.component.PopupX.Companion.popUpXOnThis
+import com.github.serivesmejia.eocvsim.gui.util.Corner
+import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer
+import com.github.serivesmejia.eocvsim.pipeline.PipelineData
+import com.github.serivesmejia.eocvsim.util.ReflectUtil
+import com.github.serivesmejia.eocvsim.util.loggerForThis
+import com.qualcomm.robotcore.eventloop.opmode.*
+import com.qualcomm.robotcore.util.Range
+import io.github.deltacv.vision.internal.opmode.OpModeState
+import java.awt.GridBagConstraints
+import java.awt.GridBagLayout
+import java.awt.event.MouseAdapter
+import java.awt.event.MouseEvent
+import javax.swing.*
+
+class OpModeSelectorPanel(val eocvSim: EOCVSim, val opModeControlsPanel: OpModeControlsPanel) : JPanel() {
+
+ private var _selectedIndex = -1
+
+ private val logger by loggerForThis()
+
+ var selectedIndex: Int
+ get() = _selectedIndex
+ set(value) {
+ opModeControlsPanel.opModeSelected(value)
+ _selectedIndex = value
+ }
+
+ private var pipelinesData = arrayOf()
+
+ //
+ private val autonomousIndexMap = mutableMapOf()
+ //
+ private val teleopIndexMap = mutableMapOf()
+
+ val autonomousButton = JButton()
+
+ val selectOpModeLabelsPanel = JPanel()
+ val opModeNameLabelPanel = JPanel()
+
+ val textPanel = JPanel()
+
+ val opModeNameLabel = JLabel("")
+
+ val selectOpModeLabel = JLabel("Select Op Mode")
+ val buttonDescriptorLabel = JLabel("<- Autonomous | TeleOp ->")
+
+ val teleopButton = JButton()
+
+ val autonomousSelector = JList()
+ val teleopSelector = JList()
+
+ var isActive = false
+ set(value) {
+ opModeControlsPanel.isActive = value
+ field = value
+ }
+
+ init {
+ layout = GridBagLayout()
+ selectOpModeLabelsPanel.layout = GridBagLayout()
+
+ autonomousSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION
+ teleopSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION
+
+ autonomousSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) { autonomousIndexMap }
+ teleopSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) { teleopIndexMap }
+
+ autonomousButton.icon = EOCVSimIconLibrary.icoArrowDropdown
+
+ add(autonomousButton, GridBagConstraints().apply {
+ gridx = 0
+ gridy = 0
+ ipady = 20
+
+ weightx = 1.0
+ anchor = GridBagConstraints.WEST
+
+ gridheight = 1
+ })
+
+ selectOpModeLabel.horizontalTextPosition = JLabel.CENTER
+ selectOpModeLabel.horizontalAlignment = JLabel.CENTER
+
+ buttonDescriptorLabel.horizontalTextPosition = JLabel.CENTER
+ buttonDescriptorLabel.horizontalAlignment = JLabel.CENTER
+
+ selectOpModeLabelsPanel.add(selectOpModeLabel, GridBagConstraints().apply {
+ gridx = 0
+ gridy = 0
+ ipady = 0
+ })
+
+ selectOpModeLabelsPanel.add(buttonDescriptorLabel, GridBagConstraints().apply {
+ gridx = 0
+ gridy = 1
+ ipadx = 10
+ })
+
+ textPanel.add(selectOpModeLabelsPanel)
+
+ opModeNameLabelPanel.add(opModeNameLabel)
+
+ add(textPanel, GridBagConstraints().apply {
+ gridx = 1
+ gridy = 0
+ ipadx = 20
+ })
+
+ teleopButton.icon = EOCVSimIconLibrary.icoArrowDropdown
+
+ add(teleopButton, GridBagConstraints().apply {
+ gridx = 2
+ gridy = 0
+ ipady = 20
+
+ weightx = 1.0
+ anchor = GridBagConstraints.EAST
+
+ gridheight = 1
+ })
+
+ registerListeners()
+ }
+
+ private fun registerListeners() {
+ autonomousButton.addActionListener {
+ val popup = autonomousButton.popUpXOnThis(OpModePopupPanel(autonomousSelector), Corner.BOTTOM_LEFT, Corner.TOP_LEFT)
+
+ opModeControlsPanel.stopCurrentOpMode()
+
+ val listSelectionListener = object : javax.swing.event.ListSelectionListener {
+ override fun valueChanged(e: javax.swing.event.ListSelectionEvent?) {
+ if(!e!!.valueIsAdjusting) {
+ popup.hide()
+ autonomousSelector.removeListSelectionListener(this)
+ }
+ }
+ }
+
+ autonomousSelector.addListSelectionListener(listSelectionListener)
+
+ popup.show()
+ }
+
+ teleopButton.addActionListener {
+ val popup = teleopButton.popUpXOnThis(OpModePopupPanel(teleopSelector), Corner.BOTTOM_RIGHT, Corner.TOP_RIGHT)
+
+ opModeControlsPanel.stopCurrentOpMode()
+
+ val listSelectionListener = object : javax.swing.event.ListSelectionListener {
+ override fun valueChanged(e: javax.swing.event.ListSelectionEvent?) {
+ if(!e!!.valueIsAdjusting) {
+ popup.hide()
+ teleopSelector.removeListSelectionListener(this)
+ }
+ }
+ }
+
+ teleopSelector.addListSelectionListener(listSelectionListener)
+
+ popup.show()
+ }
+
+ autonomousSelector.addMouseListener(object: MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent) {
+ if (!isActive) return
+
+ val index = (e.source as JList<*>).locationToIndex(e.point)
+ if(index >= 0) {
+ autonomousSelected(index)
+ }
+ }
+ })
+
+ teleopSelector.addMouseListener(object: MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent) {
+ if (!isActive) return
+
+ val index = (e.source as JList<*>).locationToIndex(e.point)
+ if(index >= 0) {
+ teleOpSelected(index)
+ }
+ }
+ })
+
+ eocvSim.pipelineManager.onPipelineChange {
+ if(!isActive) return@onPipelineChange
+
+ // we are doing this to detect external pipeline changes and reflect them
+ // accordingly in the UI.
+ //
+ // in the event that this change was triggered by us, OpModeSelectorPanel,
+ // we need to hold on a cycle so that the state has been fully updated,
+ // just to be able to check correctly and, if it was requested by
+ // OpModeSelectorPanel, skip this message and not do anything.
+ eocvSim.pipelineManager.onUpdate.doOnce {
+ if(isActive && opModeControlsPanel.currentOpMode != eocvSim.pipelineManager.currentPipeline && eocvSim.pipelineManager.currentPipeline != null) {
+ val opMode = eocvSim.pipelineManager.currentPipeline
+
+ if(opMode is OpMode) {
+ val name = if (opMode.opModeType == OpModeType.AUTONOMOUS)
+ opMode.autonomousAnnotation.name
+ else opMode.teleopAnnotation.name
+
+ logger.info("External change detected \"$name\"")
+
+ opModeSelected(eocvSim.pipelineManager.currentPipelineIndex, name, false)
+ } else if(isActive) {
+ reset(-1)
+ }
+ }
+ }
+ }
+ }
+
+ private fun teleOpSelected(index: Int) {
+ opModeSelected(teleopIndexMap[index]!!, teleopSelector.selectedValue!!)
+ }
+
+ private fun autonomousSelected(index: Int) {
+ opModeSelected(autonomousIndexMap[index]!!, autonomousSelector.selectedValue!!)
+ }
+
+ private fun opModeSelected(managerIndex: Int, name: String, forceChangePipeline: Boolean = true) {
+ if(!isActive) return
+
+ opModeNameLabel.text = name
+
+ textPanel.removeAll()
+ textPanel.add(opModeNameLabelPanel)
+
+ _selectedIndex = managerIndex
+
+ opModeControlsPanel.opModeSelected(managerIndex, forceChangePipeline)
+ }
+
+ fun updateOpModesList() {
+ val autonomousListModel = DefaultListModel()
+ val teleopListModel = DefaultListModel()
+
+ pipelinesData = eocvSim.pipelineManager.pipelines.toArray(arrayOf())
+
+ var autonomousSelectorIndex = Range.clip(autonomousListModel.size() - 1, 0, Int.MAX_VALUE)
+ var teleopSelectorIndex = Range.clip(teleopListModel.size() - 1, 0, Int.MAX_VALUE)
+
+ autonomousIndexMap.clear()
+ teleopIndexMap.clear()
+
+ for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) {
+ if(ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) {
+ val type = pipeline.clazz.opModeType
+
+ if(type == OpModeType.AUTONOMOUS) {
+ val autonomousAnnotation = pipeline.clazz.autonomousAnnotation
+
+ autonomousListModel.addElement(autonomousAnnotation.name)
+ autonomousIndexMap[autonomousSelectorIndex] = managerIndex
+ autonomousSelectorIndex++
+ } else if(type == OpModeType.TELEOP) {
+ val teleopAnnotation = pipeline.clazz.teleopAnnotation
+
+ teleopListModel.addElement(teleopAnnotation.name)
+ teleopIndexMap[teleopSelectorIndex] = managerIndex
+ teleopSelectorIndex++
+ }
+ }
+ }
+
+ autonomousSelector.fixedCellWidth = 240
+ autonomousSelector.model = autonomousListModel
+
+ teleopSelector.fixedCellWidth = 240
+ teleopSelector.model = teleopListModel
+ }
+
+ fun reset(nextPipeline: Int? = null) {
+ textPanel.removeAll()
+ textPanel.add(selectOpModeLabelsPanel)
+
+ opModeControlsPanel.reset()
+
+ val opMode = opModeControlsPanel.currentOpMode
+
+ if(eocvSim.pipelineManager.currentPipeline == opMode && opMode != null && opMode.notifier.state != OpModeState.SELECTED) {
+ opMode?.notifier?.onStateChange?.let {
+ it {
+ val state = opMode.notifier.state
+
+ if(state == OpModeState.STOPPED) {
+ it.removeThis()
+
+ if(nextPipeline == null || nextPipeline >= 0) {
+ eocvSim.pipelineManager.onUpdate.doOnce {
+ eocvSim.pipelineManager.changePipeline(nextPipeline)
+ }
+ }
+ }
+ }
+ }
+ } else if(nextPipeline == null || nextPipeline >= 0) {
+ eocvSim.pipelineManager.onUpdate.doOnce {
+ eocvSim.pipelineManager.requestChangePipeline(nextPipeline)
+ }
+ }
+
+ _selectedIndex = -1
+ opModeControlsPanel.stopCurrentOpMode()
+ }
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt
index 11bd34e7..45f8ba0b 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt
@@ -24,6 +24,7 @@
package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline
import com.github.serivesmejia.eocvsim.EOCVSim
+import com.github.serivesmejia.eocvsim.gui.DialogFactory
import com.github.serivesmejia.eocvsim.gui.component.PopupX
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
@@ -98,13 +99,34 @@ class PipelineSelectorButtonsPanel(eocvSim: EOCVSim) : JPanel(GridBagLayout()) {
})
// WORKSPACE BUTTONS POPUP
+
pipelineCompileBtt.addActionListener { eocvSim.visualizer.asyncCompilePipelines() }
- workspaceButtonsPanel.add(pipelineCompileBtt, GridBagConstraints())
+ workspaceButtonsPanel.add(pipelineCompileBtt, GridBagConstraints().apply {
+ gridx = 0
+ gridy = 0
+ })
val selectWorkspBtt = JButton("Select workspace")
selectWorkspBtt.addActionListener { eocvSim.visualizer.selectPipelinesWorkspace() }
- workspaceButtonsPanel.add(selectWorkspBtt, GridBagConstraints().apply { gridx = 1 })
+ workspaceButtonsPanel.add(selectWorkspBtt, GridBagConstraints().apply {
+ gridx = 1
+ gridy = 0
+ })
+
+ val outputBtt = JButton("Pipeline Output")
+
+ outputBtt.addActionListener { DialogFactory.createPipelineOutput(eocvSim) }
+ workspaceButtonsPanel.add(outputBtt, GridBagConstraints().apply {
+ gridy = 1
+ weightx = 1.0
+ gridwidth = 2
+
+ fill = GridBagConstraints.HORIZONTAL
+ anchor = GridBagConstraints.CENTER
+
+ insets = Insets(3, 0, 0, 0)
+ })
}
}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt
index 73b2c559..7fc835a2 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt
@@ -25,40 +25,53 @@ package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline
import com.github.serivesmejia.eocvsim.EOCVSim
import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer
+import com.github.serivesmejia.eocvsim.pipeline.PipelineData
import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
+import com.github.serivesmejia.eocvsim.util.ReflectUtil
+import com.qualcomm.robotcore.eventloop.opmode.OpMode
+import com.qualcomm.robotcore.util.Range
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
-import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
+import java.awt.event.MouseAdapter
+import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.event.ListSelectionEvent
class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
var selectedIndex: Int
- get() = pipelineSelector.selectedIndex
+ get() = indexMap[pipelineSelector.selectedIndex] ?: -1
set(value) {
runBlocking {
launch(Dispatchers.Swing) {
- pipelineSelector.selectedIndex = value
+ pipelineSelector.selectedIndex = indexMap.entries.find { it.value == value }?.key ?: -1
}
}
}
- val pipelineSelector = JList()
- val pipelineSelectorScroll = JScrollPane()
+ private var pipelinesData = arrayOf()
+
+ val pipelineSelector = JList()
+ val pipelineSelectorScroll = JScrollPane()
val pipelineSelectorLabel = JLabel("Pipelines")
+ //
+ private val indexMap = mutableMapOf()
+
val buttonsPanel = PipelineSelectorButtonsPanel(eocvSim)
var allowPipelineSwitching = false
private var beforeSelectedPipeline = -1
+ var isActive = false
+ internal set
+
init {
layout = GridBagLayout()
@@ -66,12 +79,12 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
pipelineSelectorLabel.horizontalAlignment = JLabel.CENTER
- add(pipelineSelectorLabel, GridBagConstraints().apply {
- gridy = 0
- ipady = 20
- })
+ //add(pipelineSelectorLabel, GridBagConstraints().apply {
+ // gridy = 0
+ // ipady = 20
+ //})
- pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager)
+ pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) { indexMap }
pipelineSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION
pipelineSelectorScroll.setViewportView(pipelineSelector)
@@ -79,7 +92,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
pipelineSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
add(pipelineSelectorScroll, GridBagConstraints().apply {
- gridy = 1
+ gridy = 0
weightx = 0.5
weighty = 1.0
@@ -90,7 +103,7 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
})
add(buttonsPanel, GridBagConstraints().apply {
- gridy = 2
+ gridy = 1
ipady = 20
})
@@ -98,46 +111,61 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
}
private fun registerListeners() {
+ pipelineSelector.addMouseListener(object: MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent) {
+ if (!allowPipelineSwitching) return
- //listener for changing pipeline
- pipelineSelector.addListSelectionListener { evt: ListSelectionEvent ->
- if(!allowPipelineSwitching) return@addListSelectionListener
-
- if (pipelineSelector.selectedIndex != -1) {
- val pipeline = pipelineSelector.selectedIndex
-
- if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) {
- if (!eocvSim.pipelineManager.paused) {
- eocvSim.pipelineManager.requestChangePipeline(pipeline)
- beforeSelectedPipeline = pipeline
- } else {
- if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) {
- pipelineSelector.setSelectedIndex(beforeSelectedPipeline)
- } else { //handling pausing
- eocvSim.pipelineManager.requestSetPaused(false)
+ val index = (e.source as JList<*>).locationToIndex(e.point)
+
+ if (index != -1) {
+ val pipeline = indexMap[index] ?: return
+
+ if (pipeline != beforeSelectedPipeline) {
+ if (!eocvSim.pipelineManager.paused) {
eocvSim.pipelineManager.requestChangePipeline(pipeline)
beforeSelectedPipeline = pipeline
+ } else {
+ if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) {
+ pipelineSelector.setSelectedIndex(beforeSelectedPipeline)
+ } else { //handling pausing
+ eocvSim.pipelineManager.requestSetPaused(false)
+ eocvSim.pipelineManager.requestChangePipeline(pipeline)
+ beforeSelectedPipeline = pipeline
+ }
}
}
+ } else {
+ pipelineSelector.setSelectedIndex(0)
}
- } else {
- pipelineSelector.setSelectedIndex(1)
}
+ })
+
+ eocvSim.pipelineManager.onPipelineChange {
+ selectedIndex = eocvSim.pipelineManager.currentPipelineIndex
}
}
- fun updatePipelinesList() = runBlocking {
- launch(Dispatchers.Swing) {
- val listModel = DefaultListModel()
- for (pipeline in eocvSim.pipelineManager.pipelines) {
+ fun updatePipelinesList() {
+ val listModel = DefaultListModel()
+ var selectorIndex = Range.clip(listModel.size() - 1, 0, Int.MAX_VALUE)
+
+ indexMap.clear()
+
+ pipelinesData = eocvSim.pipelineManager.pipelines.toArray(arrayOf())
+
+ for ((managerIndex, pipeline) in eocvSim.pipelineManager.pipelines.withIndex()) {
+ if (!ReflectUtil.hasSuperclass(pipeline.clazz, OpMode::class.java)) {
listModel.addElement(pipeline.clazz.simpleName)
+ indexMap[selectorIndex] = managerIndex
+
+ selectorIndex++
}
+ }
- pipelineSelector.fixedCellWidth = 240
- pipelineSelector.model = listModel
+ pipelineSelector.fixedCellWidth = 240
+ pipelineSelector.model = listModel
- revalAndRepaint()
- }
+ revalAndRepaint()
}
fun revalAndRepaint() {
@@ -147,4 +175,34 @@ class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
pipelineSelectorScroll.repaint()
}
+ fun refreshAndReselectCurrent(changePipeline: Boolean = false) {
+ val currentIndex = selectedIndex
+ val beforePipeline = pipelinesData[currentIndex]
+
+ updatePipelinesList()
+
+ val beforeSwitching = allowPipelineSwitching
+
+ if(!changePipeline) {
+ allowPipelineSwitching = false
+ }
+
+ for((i, pipeline) in pipelinesData.withIndex()) {
+ if(pipeline.clazz.name == beforePipeline.clazz.name && pipeline.source == beforePipeline.source) {
+ selectedIndex = i
+
+ if(!changePipeline) {
+ allowPipelineSwitching = beforeSwitching
+ }
+ return
+ }
+ }
+
+ selectedIndex = 0 // default pipeline
+
+ if(!changePipeline) {
+ allowPipelineSwitching = beforeSwitching
+ }
+ }
+
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt
similarity index 94%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt
rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt
index 12830556..cd4d77df 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/SourceSelectorPanel.kt
@@ -1,7 +1,8 @@
-package com.github.serivesmejia.eocvsim.gui.component.visualizer
+package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline
import com.github.serivesmejia.eocvsim.EOCVSim
import com.github.serivesmejia.eocvsim.gui.component.PopupX
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.CreateSourcePanel
import com.github.serivesmejia.eocvsim.gui.util.icon.SourcesListIconRenderer
import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero
@@ -37,10 +38,10 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
sourceSelectorLabel.font = sourceSelectorLabel.font.deriveFont(20.0f)
sourceSelectorLabel.horizontalAlignment = JLabel.CENTER
- add(sourceSelectorLabel, GridBagConstraints().apply {
- gridy = 0
- ipady = 20
- })
+ // add(sourceSelectorLabel, GridBagConstraints().apply {
+ // gridy = 0
+ // ipady = 20
+ //})
sourceSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION
@@ -49,7 +50,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
sourceSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
add(sourceSelectorScroll, GridBagConstraints().apply {
- gridy = 1
+ gridy = 0
weightx = 0.5
weighty = 1.0
@@ -82,7 +83,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() {
sourceSelectorButtonsContainer.add(sourceSelectorDeleteBtt)
add(sourceSelectorButtonsContainer, GridBagConstraints().apply {
- gridy = 2
+ gridy = 1
ipady = 20
})
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java
index 768f62a8..958a6670 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java
@@ -25,8 +25,7 @@
import com.github.serivesmejia.eocvsim.EOCVSim;
import com.github.serivesmejia.eocvsim.gui.Icons;
-import com.github.serivesmejia.eocvsim.gui.Visualizer;
-import com.github.serivesmejia.eocvsim.gui.component.ImageX;
+import io.github.deltacv.vision.external.gui.component.ImageX;
import com.github.serivesmejia.eocvsim.gui.util.GuiUtil;
import com.github.serivesmejia.eocvsim.util.StrUtil;
@@ -130,7 +129,7 @@ private void initAbout() {
osLibsList.addListSelectionListener(e -> {
if(!e.getValueIsAdjusting()) {
- String text = osLibsList.getModel().getElementAt(osLibsList.getSelectedIndex());
+ String text = osLibsList.getSelectedValue();
String[] urls = StrUtil.findUrlsInString(text);
if(urls.length > 0) {
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java
index 644ea2bb..d18ada02 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java
@@ -98,7 +98,7 @@ private void initConfiguration() {
themePanel.add(this.themeComboBox);
uiPanel.add(themePanel);
- tabbedPane.addTab("Inteface", uiPanel);
+ tabbedPane.addTab("Interface", uiPanel);
/*
INPUT SOURCES TAB
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt
index 3987a033..9ae95f79 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt
@@ -1,5 +1,6 @@
package com.github.serivesmejia.eocvsim.gui.dialog
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary
import com.github.serivesmejia.eocvsim.gui.Icons
import com.github.serivesmejia.eocvsim.util.event.EventHandler
import java.awt.*
@@ -40,7 +41,7 @@ class SplashScreen(closeHandler: EventHandler? = null) : JDialog() {
}
class ImagePanel : JPanel(GridBagLayout()) {
- val img = Icons.getImage("ico_eocvsim")
+ val img = EOCVSimIconLibrary.icoEOCVSim
init {
isOpaque = false
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java
index b7826535..e5c44d0f 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java
@@ -27,7 +27,7 @@
import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector;
import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields;
import com.github.serivesmejia.eocvsim.input.source.ImageSource;
-import com.github.serivesmejia.eocvsim.util.CvUtil;
+import io.github.deltacv.vision.external.util.CvUtil;
import com.github.serivesmejia.eocvsim.util.FileFilters;
import com.github.serivesmejia.eocvsim.util.StrUtil;
import org.opencv.core.Size;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java
index 97115b81..1c9f5464 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java
@@ -27,7 +27,7 @@
import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector;
import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields;
import com.github.serivesmejia.eocvsim.input.source.VideoSource;
-import com.github.serivesmejia.eocvsim.util.CvUtil;
+import io.github.deltacv.vision.external.util.CvUtil;
import com.github.serivesmejia.eocvsim.util.FileFilters;
import com.github.serivesmejia.eocvsim.util.StrUtil;
import org.opencv.core.Mat;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt
new file mode 100644
index 00000000..a308cb46
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/Enums.kt
@@ -0,0 +1,6 @@
+package com.github.serivesmejia.eocvsim.gui.util
+
+enum class Corner {
+ TOP_LEFT, TOP_RIGHT,
+ BOTTOM_LEFT, BOTTOM_RIGHT
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java
index a1fb63f4..5b442208 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java
@@ -26,7 +26,7 @@
import com.github.serivesmejia.eocvsim.EOCVSim;
import com.github.serivesmejia.eocvsim.gui.DialogFactory;
import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists;
-import com.github.serivesmejia.eocvsim.util.CvUtil;
+import io.github.deltacv.vision.external.util.CvUtil;
import com.github.serivesmejia.eocvsim.util.SysUtil;
import org.opencv.core.Mat;
import org.slf4j.Logger;
@@ -83,25 +83,6 @@ public void replace(FilterBypass fb, int offset, int length, String text, Attrib
}
- public static ImageIcon scaleImage(ImageIcon icon, int w, int h) {
-
- int nw = icon.getIconWidth();
- int nh = icon.getIconHeight();
-
- if (icon.getIconWidth() > w) {
- nw = w;
- nh = (nw * icon.getIconHeight()) / icon.getIconWidth();
- }
-
- if (nh > h) {
- nh = h;
- nw = (icon.getIconWidth() * nh) / icon.getIconHeight();
- }
-
- return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_SMOOTH));
-
- }
-
public static ImageIcon loadImageIcon(String path) throws IOException {
return new ImageIcon(loadBufferedImage(path));
}
@@ -204,14 +185,12 @@ public static void saveBufferedImageFileChooser(Component parent, BufferedImage
}
public static void saveMatFileChooser(Component parent, Mat mat, EOCVSim eocvSim) {
-
Mat clonedMat = mat.clone();
BufferedImage img = CvUtil.matToBufferedImage(clonedMat);
clonedMat.release();
saveBufferedImageFileChooser(parent, img, eocvSim);
-
}
public static ListModel isToListModel(InputStream is, Charset charset) throws UnsupportedEncodingException {
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt
index f9c68161..c70c3cc2 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt
@@ -3,6 +3,7 @@ package com.github.serivesmejia.eocvsim.gui.util
import java.awt.Image
import java.lang.reflect.InvocationTargetException
+@Deprecated("Use JDK 9 Taskbar API instead, this used to be a workaround when EOCV-Sim targeted JDK 8 before v3.4.0")
object ReflectTaskbar {
private val taskbarClass by lazy { Class.forName("java.awt.Taskbar") }
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java
similarity index 90%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java
rename to EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java
index a8638aeb..d38c12fe 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ThreadedMatPoster.java
@@ -1,220 +1,221 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.gui.util;
-
-import com.github.serivesmejia.eocvsim.util.fps.FpsCounter;
-import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue;
-import org.opencv.core.Mat;
-import org.openftc.easyopencv.MatRecycler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.concurrent.ArrayBlockingQueue;
-
-public class MatPoster {
-
- private final ArrayList postables = new ArrayList<>();
-
- private final EvictingBlockingQueue postQueue;
- private final MatRecycler matRecycler;
-
- private final String name;
-
- private final Thread posterThread;
-
- public final FpsCounter fpsCounter = new FpsCounter();
-
- private final Object lock = new Object();
-
- private volatile boolean paused = false;
-
- private volatile boolean hasPosterThreadStarted = false;
-
- Logger logger;
-
- public static MatPoster createWithoutRecycler(String name, int maxQueueItems) {
- return new MatPoster(name, maxQueueItems, null);
- }
-
- public MatPoster(String name, int maxQueueItems) {
- this(name, new MatRecycler(maxQueueItems + 2));
- }
-
- public MatPoster(String name, MatRecycler recycler) {
- this(name, recycler.getSize(), recycler);
- }
-
- public MatPoster(String name, int maxQueueItems, MatRecycler recycler) {
- postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems));
- matRecycler = recycler;
- posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread");
-
- this.name = name;
-
- logger = LoggerFactory.getLogger("MatPoster-" + name);
-
- postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue
- }
-
- public void post(Mat m) {
- if (m == null || m.empty()) {
- logger.warn("Tried to post empty or null mat, skipped this frame.");
- return;
- }
-
- if (matRecycler != null) {
- if(matRecycler.getAvailableMatsAmount() < 1) {
- //evict one if we don't have any available mats in the recycler
- evict(postQueue.poll());
- }
-
- MatRecycler.RecyclableMat recycledMat = matRecycler.takeMat();
- m.copyTo(recycledMat);
-
- postQueue.offer(recycledMat);
- } else {
- postQueue.offer(m);
- }
- }
-
- public void synchronizedPost(Mat m) {
- synchronize(() -> post(m));
- }
-
- public Mat pull() throws InterruptedException {
- synchronized(lock) {
- return postQueue.take();
- }
- }
-
- public void clearQueue() {
- if(postQueue.size() == 0) return;
-
- synchronized(lock) {
- postQueue.clear();
- }
- }
-
- public void synchronize(Runnable runn) {
- synchronized(lock) {
- runn.run();
- }
- }
-
- public void addPostable(Postable postable) {
- //start mat posting thread if it hasn't been started yet
- if (!posterThread.isAlive() && !hasPosterThreadStarted) {
- posterThread.start();
- }
-
- postables.add(postable);
- }
-
- public void stop() {
- logger.info("Destroying...");
-
- posterThread.interrupt();
-
- for (Mat m : postQueue) {
- if (m != null) {
- if(m instanceof MatRecycler.RecyclableMat) {
- ((MatRecycler.RecyclableMat)m).returnMat();
- }
- }
- }
-
- matRecycler.releaseAll();
- }
-
- private void evict(Mat m) {
- if (m instanceof MatRecycler.RecyclableMat) {
- ((MatRecycler.RecyclableMat) m).returnMat();
- }
- m.release();
- }
-
- public void setPaused(boolean paused) {
- this.paused = paused;
- }
-
- public boolean getPaused() {
- synchronized(lock) {
- return paused;
- }
- }
-
- public String getName() {
- return name;
- }
-
- public interface Postable {
- void post(Mat m);
- }
-
- private class PosterRunnable implements Runnable {
-
- private Mat postableMat = new Mat();
-
- @Override
- public void run() {
- hasPosterThreadStarted = true;
-
- while (!Thread.interrupted()) {
-
- while(paused && !Thread.currentThread().isInterrupted()) {
- Thread.yield();
- }
-
- if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames
-
- synchronized(lock) {
- fpsCounter.update();
-
- try {
- Mat takenMat = postQueue.take();
-
- for (Postable postable : postables) {
- takenMat.copyTo(postableMat);
- postable.post(postableMat);
- }
-
- takenMat.release();
-
- if (takenMat instanceof MatRecycler.RecyclableMat) {
- ((MatRecycler.RecyclableMat) takenMat).returnMat();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- break;
- } catch (Exception ex) { }
- }
-
- }
-
- logger.warn("Thread interrupted (" + Integer.toHexString(hashCode()) + ")");
- }
- }
-
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.gui.util;
+
+import com.github.serivesmejia.eocvsim.util.fps.FpsCounter;
+import io.github.deltacv.common.image.MatPoster;
+import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue;
+import org.opencv.core.Mat;
+import org.openftc.easyopencv.MatRecycler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.concurrent.ArrayBlockingQueue;
+
+public class ThreadedMatPoster implements MatPoster {
+ private final ArrayList postables = new ArrayList<>();
+
+ private final EvictingBlockingQueue postQueue;
+ private final MatRecycler matRecycler;
+
+ private final String name;
+
+ private final Thread posterThread;
+
+ public final FpsCounter fpsCounter = new FpsCounter();
+
+ private final Object lock = new Object();
+
+ private volatile boolean paused = false;
+
+ private volatile boolean hasPosterThreadStarted = false;
+
+ Logger logger;
+
+ public static ThreadedMatPoster createWithoutRecycler(String name, int maxQueueItems) {
+ return new ThreadedMatPoster(name, maxQueueItems, null);
+ }
+
+ public ThreadedMatPoster(String name, int maxQueueItems) {
+ this(name, new MatRecycler(maxQueueItems + 2));
+ }
+
+ public ThreadedMatPoster(String name, MatRecycler recycler) {
+ this(name, recycler.getSize(), recycler);
+ }
+
+ public ThreadedMatPoster(String name, int maxQueueItems, MatRecycler recycler) {
+ postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems));
+ matRecycler = recycler;
+ posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread");
+
+ this.name = name;
+
+ logger = LoggerFactory.getLogger("MatPoster-" + name);
+
+ postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue
+ }
+
+ @Override
+ public void post(Mat m, Object context) {
+ if (m == null || m.empty()) {
+ logger.warn("Tried to post empty or null mat, skipped this frame.");
+ return;
+ }
+
+ if (matRecycler != null) {
+ if(matRecycler.getAvailableMatsAmount() < 1) {
+ //evict one if we don't have any available mats in the recycler
+ evict(postQueue.poll());
+ }
+
+ MatRecycler.RecyclableMat recycledMat = matRecycler.takeMatOrNull();
+ m.copyTo(recycledMat);
+
+ postQueue.offer(recycledMat);
+ } else {
+ postQueue.offer(m);
+ }
+ }
+
+ public void synchronizedPost(Mat m) {
+ synchronize(() -> post(m));
+ }
+
+ public Mat pull() throws InterruptedException {
+ synchronized(lock) {
+ return postQueue.take();
+ }
+ }
+
+ public void clearQueue() {
+ if(postQueue.size() == 0) return;
+
+ synchronized(lock) {
+ postQueue.clear();
+ }
+ }
+
+ public void synchronize(Runnable runn) {
+ synchronized(lock) {
+ runn.run();
+ }
+ }
+
+ public void addPostable(Postable postable) {
+ //start mat posting thread if it hasn't been started yet
+ if (!posterThread.isAlive() && !hasPosterThreadStarted) {
+ posterThread.start();
+ }
+
+ postables.add(postable);
+ }
+
+ public void stop() {
+ logger.info("Destroying...");
+
+ posterThread.interrupt();
+
+ for (Mat m : postQueue) {
+ if (m != null) {
+ if(m instanceof MatRecycler.RecyclableMat) {
+ ((MatRecycler.RecyclableMat)m).returnMat();
+ }
+ }
+ }
+
+ matRecycler.releaseAll();
+ }
+
+ private void evict(Mat m) {
+ if (m instanceof MatRecycler.RecyclableMat) {
+ ((MatRecycler.RecyclableMat) m).returnMat();
+ }
+ m.release();
+ }
+
+ public void setPaused(boolean paused) {
+ this.paused = paused;
+ }
+
+ public boolean getPaused() {
+ synchronized(lock) {
+ return paused;
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public interface Postable {
+ void post(Mat m);
+ }
+
+ private class PosterRunnable implements Runnable {
+
+ private Mat postableMat = new Mat();
+
+ @Override
+ public void run() {
+ hasPosterThreadStarted = true;
+
+ while (!Thread.interrupted()) {
+
+ while(paused && !Thread.currentThread().isInterrupted()) {
+ Thread.yield();
+ }
+
+ if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames
+
+ synchronized(lock) {
+ fpsCounter.update();
+
+ try {
+ Mat takenMat = postQueue.take();
+
+ for (Postable postable : postables) {
+ takenMat.copyTo(postableMat);
+ postable.post(postableMat);
+ }
+
+ takenMat.release();
+
+ if (takenMat instanceof MatRecycler.RecyclableMat) {
+ ((MatRecycler.RecyclableMat) takenMat).returnMat();
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ break;
+ } catch (Exception ex) { }
+ }
+
+ }
+
+ logger.warn("Thread interrupted (" + Integer.toHexString(hashCode()) + ")");
+ }
+ }
+
}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt
index 94f6e107..71081a42 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt
@@ -1,5 +1,6 @@
package com.github.serivesmejia.eocvsim.gui.util.icon
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary
import com.github.serivesmejia.eocvsim.gui.Icons
import com.github.serivesmejia.eocvsim.input.InputSourceManager
import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
@@ -9,11 +10,12 @@ import javax.swing.*
import java.awt.*
class PipelineListIconRenderer(
- private val pipelineManager: PipelineManager
+ private val pipelineManager: PipelineManager,
+ private val indexMapProvider: () -> Map
) : DefaultListCellRenderer() {
- private val gearsIcon by Icons.lazyGetImageResized("ico_gears", 15, 15)
- private val hammerIcon by Icons.lazyGetImageResized("ico_hammer", 15, 15)
+ private val gearsIcon by EOCVSimIconLibrary.icoGears.lazyResized(15, 15)
+ private val hammerIcon by EOCVSimIconLibrary.icoHammer.lazyResized(15, 15)
override fun getListCellRendererComponent(
list: JList<*>,
@@ -26,17 +28,11 @@ class PipelineListIconRenderer(
list, value, index, isSelected, cellHasFocus
) as JLabel
- val runtimePipelinesAmount = pipelineManager.getPipelinesFrom(
- PipelineSource.COMPILED_ON_RUNTIME
- ).size
+ val source = pipelineManager.pipelines[indexMapProvider()[index]!!].source
- if(runtimePipelinesAmount > 0) {
- val source = pipelineManager.pipelines[index].source
-
- label.icon = when(source) {
- PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon
- else -> hammerIcon
- }
+ label.icon = when(source) {
+ PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon
+ else -> hammerIcon
}
return label
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java
index dd582038..d56f540a 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java
@@ -23,6 +23,7 @@
package com.github.serivesmejia.eocvsim.gui.util.icon;
+import com.github.serivesmejia.eocvsim.gui.EOCVSimIconLibrary;
import com.github.serivesmejia.eocvsim.gui.Icons;
import com.github.serivesmejia.eocvsim.input.InputSourceManager;
@@ -59,19 +60,19 @@ public Component getListCellRendererComponent(
switch (sourceManager.getSourceType((String) value)) {
case IMAGE:
if(imageIcon == null) {
- imageIcon = Icons.INSTANCE.getImageResized("ico_img", 15, 15);
+ imageIcon = EOCVSimIconLibrary.INSTANCE.getIcoImg().resized(15, 15);
}
label.setIcon(imageIcon);
break;
case CAMERA:
if(camIcon == null) {
- camIcon = Icons.INSTANCE.getImageResized("ico_cam", 15, 15);
+ camIcon = EOCVSimIconLibrary.INSTANCE.getIcoCam().resized(15, 15);
}
label.setIcon(camIcon);
break;
case VIDEO:
if(vidIcon == null) {
- vidIcon = Icons.INSTANCE.getImageResized("ico_vid", 15, 15);
+ vidIcon = EOCVSimIconLibrary.INSTANCE.getIcoVid().resized(15, 15);
}
label.setIcon(vidIcon);
break;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java
index a1a384a7..e8f394d0 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java
@@ -25,33 +25,39 @@
import com.github.serivesmejia.eocvsim.EOCVSim;
import org.opencv.core.Mat;
+import org.opencv.core.Size;
import javax.swing.filechooser.FileFilter;
public abstract class InputSource implements Comparable {
- public transient boolean isDefault = false;
- public transient EOCVSim eocvSim = null;
+ public transient boolean isDefault;
+ public transient EOCVSim eocvSim;
protected transient String name = "";
- protected transient boolean isPaused = false;
- private transient boolean beforeIsPaused = false;
+ protected transient boolean isPaused;
+ private transient boolean beforeIsPaused;
- protected long createdOn = -1L;
+ protected transient long createdOn = -1L;
public abstract boolean init();
public abstract void reset();
+
+ public void cleanIfDirty() { }
+
public abstract void close();
public abstract void onPause();
public abstract void onResume();
+ public void setSize(Size size) {}
+
public Mat update() {
return null;
}
public final InputSource cloneSource() {
- InputSource source = internalCloneSource();
+ final InputSource source = internalCloneSource();
source.createdOn = createdOn;
return source;
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java
index d025f4b3..eb759b80 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java
@@ -25,7 +25,7 @@
import com.github.serivesmejia.eocvsim.EOCVSim;
import com.github.serivesmejia.eocvsim.gui.Visualizer;
-import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel;
+import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.SourceSelectorPanel;
import com.github.serivesmejia.eocvsim.input.source.ImageSource;
import com.github.serivesmejia.eocvsim.pipeline.PipelineManager;
import com.github.serivesmejia.eocvsim.util.SysUtil;
@@ -69,7 +69,7 @@ public void init() {
if(lastMatFromSource == null)
lastMatFromSource = new Mat();
- Size size = new Size(320, 240);
+ Size size = new Size(640, 480);
createDefaultImgInputSource("/images/ug_4.jpg", "ug_eocvsim_4.jpg", "Ultimate Goal 4 Ring", size);
createDefaultImgInputSource("/images/ug_1.jpg", "ug_eocvsim_1.jpg", "Ultimate Goal 1 Ring", size);
createDefaultImgInputSource("/images/ug_0.jpg", "ug_eocvsim_0.jpg", "Ultimate Goal 0 Ring", size);
@@ -79,13 +79,13 @@ public void init() {
inputSourceLoader.loadInputSourcesFromFile();
for (Map.Entry entry : inputSourceLoader.loadedInputSources.entrySet()) {
+ logger.info("Loaded input source " + entry.getKey());
addInputSource(entry.getKey(), entry.getValue());
}
}
private void createDefaultImgInputSource(String resourcePath, String fileName, String sourceName, Size imgSize) {
try {
-
InputStream is = InputSource.class.getResourceAsStream(resourcePath);
File f = SysUtil.copyFileIsTemp(is, fileName, true).file;
@@ -94,7 +94,6 @@ private void createDefaultImgInputSource(String resourcePath, String fileName, S
src.createdOn = sources.size();
addInputSource(sourceName, src);
-
} catch (IOException e) {
e.printStackTrace();
}
@@ -107,6 +106,7 @@ public void update(boolean isPaused) {
currentInputSource.setPaused(isPaused);
Mat m = currentInputSource.update();
+
if(m != null && !m.empty()) {
m.copyTo(lastMatFromSource);
// add an extra alpha channel because that's what eocv returns for some reason... (more realistic simulation lol)
@@ -245,7 +245,12 @@ public boolean setInputSource(String sourceName) {
logger.info("Set InputSource to " + currentInputSource.toString() + " (" + src.getClass().getSimpleName() + ")");
return true;
+ }
+ public void cleanSourceIfDirty() {
+ if(currentInputSource != null) {
+ currentInputSource.cleanIfDirty();
+ }
}
public boolean isNameOnUse(String name) {
@@ -302,6 +307,10 @@ public Visualizer.AsyncPleaseWaitDialog showApwdIfNeeded(String sourceName) {
return apwd;
}
+ public String getDefaultInputSource() {
+ return defaultSource;
+ }
+
public SourceType getSourceType(String sourceName) {
if(sourceName == null) {
return SourceType.UNKNOWN;
@@ -322,4 +331,4 @@ public InputSource[] getSortedInputSources() {
return sources.toArray(new InputSource[0]);
}
-}
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java
index c0565c6b..3998454a 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java
@@ -100,6 +100,11 @@ public CameraSource(int webcamIndex, Size size) {
isLegacyByIndex = true;
}
+ @Override
+ public void setSize(Size size) {
+ this.size = size;
+ }
+
@Override
public boolean init() {
if (initialized) return false;
@@ -146,7 +151,7 @@ public boolean init() {
}
if (matRecycler == null) matRecycler = new MatRecycler(4);
- MatRecycler.RecyclableMat newFrame = matRecycler.takeMat();
+ MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull();
camera.read(newFrame);
@@ -182,8 +187,15 @@ public void close() {
currentWebcamIndex = -1;
}
+ private MatRecycler.RecyclableMat lastNewFrame = null;
+
@Override
public Mat update() {
+ if(lastNewFrame != null) {
+ lastNewFrame.returnMat();
+ lastNewFrame = null;
+ }
+
if (isPaused) {
return lastFramePaused;
} else if (lastFramePaused != null) {
@@ -192,10 +204,11 @@ public Mat update() {
lastFramePaused = null;
}
- if (lastFrame == null) lastFrame = matRecycler.takeMat();
+ if (lastFrame == null) lastFrame = matRecycler.takeMatOrNull();
if (camera == null) return lastFrame;
- MatRecycler.RecyclableMat newFrame = matRecycler.takeMat();
+ MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull();
+ lastNewFrame = newFrame;
camera.read(newFrame);
capTimeNanos = System.nanoTime();
@@ -214,13 +227,15 @@ public Mat update() {
newFrame.release();
newFrame.returnMat();
+ lastNewFrame = null;
+
return lastFrame;
}
@Override
public void onPause() {
if (lastFrame != null) lastFrame.release();
- if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat();
+ if (lastFramePaused == null) lastFramePaused = matRecycler.takeMatOrNull();
camera.read(lastFramePaused);
Imgproc.cvtColor(lastFramePaused, lastFramePaused, Imgproc.COLOR_BGR2RGB);
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java
index 39187cd6..049b64bd 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java
@@ -121,7 +121,7 @@ public void readImage() {
Mat readMat = Imgcodecs.imread(this.imgPath);
- if (img == null) img = matRecycler.takeMat();
+ if (img == null) img = matRecycler.takeMatOrNull();
if (readMat.empty()) {
return;
@@ -142,16 +142,19 @@ public void readImage() {
@Override
public Mat update() {
-
- if (isPaused) return lastCloneTo;
- if (lastCloneTo == null) lastCloneTo = matRecycler.takeMat();
+ if (lastCloneTo == null) lastCloneTo = matRecycler.takeMatOrNull();
if (img == null) return null;
+ lastCloneTo.release();
img.copyTo(lastCloneTo);
return lastCloneTo;
+ }
+ @Override
+ public void cleanIfDirty() {
+ readImage();
}
@Override
@@ -159,6 +162,11 @@ protected InputSource internalCloneSource() {
return new ImageSource(imgPath, size);
}
+ @Override
+ public void setSize(Size size) {
+ this.size = size;
+ }
+
@Override
public FileFilter getFileFilters() {
return FileFilters.imagesFilter;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java
index aad57415..27021ef9 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java
@@ -23,9 +23,9 @@
package com.github.serivesmejia.eocvsim.input.source;
-import com.github.serivesmejia.eocvsim.gui.Visualizer;
import com.github.serivesmejia.eocvsim.input.InputSource;
import com.github.serivesmejia.eocvsim.util.FileFilters;
+import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter;
import com.google.gson.annotations.Expose;
import org.opencv.core.Mat;
import org.opencv.core.Size;
@@ -37,19 +37,20 @@
import org.slf4j.LoggerFactory;
import javax.swing.filechooser.FileFilter;
-import java.util.Objects;
public class VideoSource extends InputSource {
@Expose
private String videoPath = null;
- private transient VideoCapture video = null;
+ private transient VideoCapture video;
- private transient MatRecycler.RecyclableMat lastFramePaused = null;
- private transient MatRecycler.RecyclableMat lastFrame = null;
+ private transient FpsLimiter fpsLimiter = new FpsLimiter(30);
- private transient boolean initialized = false;
+ private transient MatRecycler.RecyclableMat lastFramePaused;
+ private transient MatRecycler.RecyclableMat lastFrame;
+
+ private transient boolean initialized;
@Expose
private volatile Size size;
@@ -60,9 +61,10 @@ public class VideoSource extends InputSource {
private transient long capTimeNanos = 0;
- Logger logger = LoggerFactory.getLogger(getClass());
+ private transient Logger logger = LoggerFactory.getLogger(getClass());
- public VideoSource() {}
+ public VideoSource() {
+ }
public VideoSource(String videoPath, Size size) {
this.videoPath = videoPath;
@@ -71,7 +73,6 @@ public VideoSource(String videoPath, Size size) {
@Override
public boolean init() {
-
if (initialized) return false;
initialized = true;
@@ -85,7 +86,7 @@ public boolean init() {
if (matRecycler == null) matRecycler = new MatRecycler(4);
- MatRecycler.RecyclableMat newFrame = matRecycler.takeMat();
+ MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull();
newFrame.release();
video.read(newFrame);
@@ -95,48 +96,44 @@ public boolean init() {
return false;
}
+ fpsLimiter.setMaxFPS(video.get(Videoio.CAP_PROP_FPS));
+
newFrame.release();
matRecycler.returnMat(newFrame);
return true;
-
}
@Override
public void reset() {
-
if (!initialized) return;
if (video != null && video.isOpened()) video.release();
- if(lastFrame != null && lastFrame.isCheckedOut())
+ if (lastFrame != null && lastFrame.isCheckedOut())
lastFrame.returnMat();
- if(lastFramePaused != null && lastFramePaused.isCheckedOut())
+ if (lastFramePaused != null && lastFramePaused.isCheckedOut())
lastFramePaused.returnMat();
matRecycler.releaseAll();
video = null;
initialized = false;
-
}
@Override
public void close() {
-
- if(video != null && video.isOpened()) video.release();
- if(lastFrame != null) lastFrame.returnMat();
+ if (video != null && video.isOpened()) video.release();
+ if (lastFrame != null && lastFrame.isCheckedOut()) lastFrame.returnMat();
if (lastFramePaused != null) {
lastFramePaused.returnMat();
lastFramePaused = null;
}
-
}
@Override
public Mat update() {
-
if (isPaused) {
return lastFramePaused;
} else if (lastFramePaused != null) {
@@ -144,10 +141,16 @@ public Mat update() {
lastFramePaused = null;
}
- if (lastFrame == null) lastFrame = matRecycler.takeMat();
+ try {
+ fpsLimiter.sync();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ if (lastFrame == null) lastFrame = matRecycler.takeMatOrNull();
if (video == null) return lastFrame;
- MatRecycler.RecyclableMat newFrame = matRecycler.takeMat();
+ MatRecycler.RecyclableMat newFrame = matRecycler.takeMatOrNull();
video.read(newFrame);
capTimeNanos = System.nanoTime();
@@ -158,7 +161,9 @@ public Mat update() {
//in next update
if (newFrame.empty()) {
newFrame.returnMat();
- video.set(Videoio.CAP_PROP_POS_FRAMES, 0);
+
+ this.reset();
+ this.init();
return lastFrame;
}
@@ -170,14 +175,12 @@ public Mat update() {
matRecycler.returnMat(newFrame);
return lastFrame;
-
}
@Override
public void onPause() {
-
if (lastFrame != null) lastFrame.release();
- if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat();
+ if (lastFramePaused == null) lastFramePaused = matRecycler.takeMatOrNull();
video.read(lastFramePaused);
@@ -190,7 +193,6 @@ public void onPause() {
video.release();
video = null;
-
}
@Override
@@ -205,6 +207,11 @@ protected InputSource internalCloneSource() {
return new VideoSource(videoPath, size);
}
+ @Override
+ public void setSize(Size size) {
+ this.size = size;
+ }
+
@Override
public FileFilter getFileFilters() {
return FileFilters.videoMediaFilter;
@@ -220,4 +227,4 @@ public String toString() {
return "VideoSource(" + videoPath + ", " + (size != null ? size.toString() : "null") + ")";
}
-}
\ No newline at end of file
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt
index 81e0d6d9..d972600f 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt
@@ -23,11 +23,12 @@
package com.github.serivesmejia.eocvsim.output
-import com.github.serivesmejia.eocvsim.gui.util.MatPoster
+import com.github.serivesmejia.eocvsim.gui.util.ThreadedMatPoster
import com.github.serivesmejia.eocvsim.util.StrUtil
-import com.github.serivesmejia.eocvsim.util.extension.aspectRatio
-import com.github.serivesmejia.eocvsim.util.extension.clipTo
+import io.github.deltacv.vision.external.util.extension.aspectRatio
+import io.github.deltacv.vision.external.util.extension.clipTo
import com.github.serivesmejia.eocvsim.util.fps.FpsCounter
+import io.github.deltacv.common.image.MatPoster
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import org.opencv.videoio.VideoWriter
@@ -47,7 +48,7 @@ class VideoRecordingSession(
@Volatile private var videoMat: Mat? = null
- val matPoster = MatPoster("VideoRec", videoFps.toInt())
+ val matPoster = ThreadedMatPoster("VideoRec", videoFps.toInt())
private val fpsCounter = FpsCounter()
@@ -62,12 +63,12 @@ class VideoRecordingSession(
matPoster.addPostable { postMat(it) }
}
- fun startRecordingSession() {
+ @Synchronized fun startRecordingSession() {
videoWriter.open(tempFile.toString(), VideoWriter.fourcc('M', 'J', 'P', 'G'), videoFps, videoSize)
hasStarted = true;
}
- fun stopRecordingSession() {
+ @Synchronized fun stopRecordingSession() {
videoWriter.release(); videoMat?.release(); matPoster.stop()
hasStopped = true
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java
index 121321f2..47f0b24d 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java
@@ -23,6 +23,8 @@
package com.github.serivesmejia.eocvsim.pipeline;
+import android.graphics.*;
+import android.graphics.Rect;
import org.firstinspires.ftc.robotcore.external.Telemetry;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
@@ -34,8 +36,21 @@ public class DefaultPipeline extends OpenCvPipeline {
private Telemetry telemetry;
+ private Paint boxPaint;
+ private Paint textPaint;
+
public DefaultPipeline(Telemetry telemetry) {
this.telemetry = telemetry;
+
+ textPaint = new Paint();
+ textPaint.setColor(Color.WHITE);
+ textPaint.setTypeface(Typeface.DEFAULT_ITALIC);
+ textPaint.setTextSize(30);
+ textPaint.setAntiAlias(true);
+
+ boxPaint = new Paint();
+ boxPaint.setColor(Color.BLACK);
+ boxPaint.setStyle(Paint.Style.FILL);
}
@Override
@@ -51,32 +66,17 @@ public Mat processFrame(Mat input) {
if (blur > 0 && blur % 2 == 1) {
Imgproc.GaussianBlur(input, input, new Size(blur, blur), 0);
+ } else if (blur > 0) {
+ Imgproc.GaussianBlur(input, input, new Size(blur + 1, blur + 1), 0);
}
- // Outline
- Imgproc.putText(
- input,
- "Default pipeline selected",
- new Point(0, 22 * aspectRatioPercentage),
- Imgproc.FONT_HERSHEY_PLAIN,
- 2 * aspectRatioPercentage,
- new Scalar(255, 255, 255),
- (int) Math.round(5 * aspectRatioPercentage)
- );
-
- //Text
- Imgproc.putText(
- input,
- "Default pipeline selected",
- new Point(0, 22 * aspectRatioPercentage),
- Imgproc.FONT_HERSHEY_PLAIN,
- 2 * aspectRatioPercentage,
- new Scalar(0, 0, 0),
- (int) Math.round(2 * aspectRatioPercentage)
- );
-
return input;
+ }
+ @Override
+ public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) {
+ canvas.drawRect(new Rect(0, 0, 385, 45), boxPaint);
+ canvas.drawText("Default pipeline selected", 5, 33, textPaint);
}
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt
index db057edc..41aaf1ea 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt
@@ -25,21 +25,30 @@ package com.github.serivesmejia.eocvsim.pipeline
import com.github.serivesmejia.eocvsim.EOCVSim
import com.github.serivesmejia.eocvsim.gui.DialogFactory
-import com.github.serivesmejia.eocvsim.gui.util.MatPoster
import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager
+import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler
+import com.github.serivesmejia.eocvsim.pipeline.instantiator.DefaultPipelineInstantiator
+import com.github.serivesmejia.eocvsim.pipeline.instantiator.PipelineInstantiator
+import com.github.serivesmejia.eocvsim.pipeline.instantiator.processor.ProcessorInstantiator
import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker
import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot
+import com.github.serivesmejia.eocvsim.util.ReflectUtil
import com.github.serivesmejia.eocvsim.util.StrUtil
import com.github.serivesmejia.eocvsim.util.event.EventHandler
import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException
import com.github.serivesmejia.eocvsim.util.fps.FpsCounter
import com.github.serivesmejia.eocvsim.util.loggerForThis
+import io.github.deltacv.common.image.MatPoster
+import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator
import kotlinx.coroutines.*
import org.firstinspires.ftc.robotcore.external.Telemetry
import org.firstinspires.ftc.robotcore.internal.opmode.TelemetryImpl
+import org.firstinspires.ftc.vision.VisionProcessor
import org.opencv.core.Mat
import org.openftc.easyopencv.OpenCvPipeline
-import org.openftc.easyopencv.TimestampedPipelineHandler
+import org.openftc.easyopencv.OpenCvViewport
+import org.openftc.easyopencv.processFrameInternal
+import java.lang.RuntimeException
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.util.*
@@ -47,7 +56,7 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.roundToLong
@OptIn(DelicateCoroutinesApi::class)
-class PipelineManager(var eocvSim: EOCVSim) {
+class PipelineManager(var eocvSim: EOCVSim, val pipelineStatisticsCalculator: PipelineStatisticsCalculator) {
companion object {
const val MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS = 5
@@ -77,12 +86,17 @@ class PipelineManager(var eocvSim: EOCVSim) {
private set
@Volatile var currentPipelineData: PipelineData? = null
private set
+ var currentTunerTarget: Any? = null
+ private set
var currentPipelineName = ""
private set
var currentPipelineIndex = -1
private set
var previousPipelineIndex = 0
+ @Volatile var previousPipeline: OpenCvPipeline? = null
+ private set
+
val activePipelineContexts = ArrayList()
private var currentPipelineContext: ExecutorCoroutineDispatcher? = null
@@ -96,6 +110,8 @@ class PipelineManager(var eocvSim: EOCVSim) {
return field
}
+ var pauseOnImages = true
+
var pauseReason = PauseReason.NOT_PAUSED
private set
get() {
@@ -109,6 +125,8 @@ class PipelineManager(var eocvSim: EOCVSim) {
var lastInitialSnapshot: PipelineSnapshot? = null
private set
+ var applyLatestSnapshotOnChange = false
+
val snapshotFieldFilter: (Field) -> Boolean = {
// only snapshot fields managed by the variable tuner
// when getTunableFieldOf returns null, it means that
@@ -119,8 +137,10 @@ class PipelineManager(var eocvSim: EOCVSim) {
//manages and builds pipelines in runtime
@JvmField val compiledPipelineManager = CompiledPipelineManager(this)
- //this will be handling the special pipeline "timestamped" type
- val timestampedPipelineHandler = TimestampedPipelineHandler()
+
+ private val pipelineHandlers = mutableListOf()
+ private val pipelineInstantiators = mutableMapOf, PipelineInstantiator>()
+
//counting and tracking exceptions for logging and reporting purposes
val pipelineExceptionTracker = PipelineExceptionTracker(this)
@@ -147,6 +167,11 @@ class PipelineManager(var eocvSim: EOCVSim) {
logger.info("Found " + pipelines.size + " pipeline(s)")
+ // add instantiator for OpenCvPipeline
+ addInstantiator(OpenCvPipeline::class.java, DefaultPipelineInstantiator)
+ // add instantiator for VisionProcessor (wraps a VisionProcessor around an OpenCvPipeline)
+ addInstantiator(VisionProcessor::class.java, ProcessorInstantiator)
+
// changing to initial pipeline
onUpdate.doOnce {
if(compiledPipelineManager.isBuildRunning && staticSnapshot != null)
@@ -180,8 +205,22 @@ class PipelineManager(var eocvSim: EOCVSim) {
}
}
+ onUpdate {
+ if(currentPipeline != null) {
+ for (pipelineHandler in pipelineHandlers) {
+ pipelineHandler.processFrame(eocvSim.inputSourceManager.currentInputSource)
+ }
+ }
+ }
+
onPipelineChange {
openedPipelineOutputCount = 0
+
+ if(currentPipeline != null) {
+ for (pipelineHandler in pipelineHandlers) {
+ pipelineHandler.onChange(previousPipeline, currentPipeline!!, currentTelemetry!!)
+ }
+ }
}
}
@@ -198,7 +237,7 @@ class PipelineManager(var eocvSim: EOCVSim) {
}
}
- eocvSim.visualizer.pipelineSelectorPanel.allowPipelineSwitching = true
+ eocvSim.visualizer.pipelineOpModeSwitchablePanel.enableSwitchingBlocking()
}
}
@@ -234,14 +273,14 @@ class PipelineManager(var eocvSim: EOCVSim) {
return
}
- timestampedPipelineHandler.update(currentPipeline, eocvSim.inputSourceManager.currentInputSource)
-
lastPipelineAction = if(!hasInitCurrentPipeline) {
"init/processFrame"
} else {
"processFrame"
}
+ pipelineStatisticsCalculator.newPipelineFrameStart()
+
//run our pipeline in the background until it finishes or gets cancelled
val pipelineJob = GlobalScope.launch(currentPipelineContext!!) {
try {
@@ -254,33 +293,47 @@ class PipelineManager(var eocvSim: EOCVSim) {
//a different pipeline at this point. we also call init if we
//haven't done so.
- if(!hasInitCurrentPipeline && inputMat != null) {
- currentPipeline?.init(inputMat)
-
- logger.info("Initialized pipeline $currentPipelineName")
-
- hasInitCurrentPipeline = true
- }
-
//check if we're still active (not timeouted)
//after initialization
if(inputMat != null) {
- currentPipeline?.processFrame(inputMat)?.let { outputMat ->
+ if(!hasInitCurrentPipeline) {
+ for(pipeHandler in pipelineHandlers) {
+ pipeHandler.preInit();
+ }
+ }
+
+ pipelineStatisticsCalculator.beforeProcessFrame()
+
+ val pipelineResult = currentPipeline?.processFrameInternal(inputMat)
+
+ pipelineStatisticsCalculator.afterProcessFrame()
+
+ pipelineResult?.let { outputMat ->
if (isActive) {
pipelineFpsCounter.update()
for (poster in pipelineOutputPosters.toTypedArray()) {
try {
- poster.post(outputMat)
+ poster.post(outputMat, OpenCvViewport.FrameContext(currentPipeline, currentPipeline?.userContextForDrawHook))
} catch (ex: Exception) {
logger.error(
- "Uncaught exception thrown while posting pipeline output Mat to ${poster.name} poster",
+ "Uncaught exception thrown while posting pipeline output Mat to poster",
ex
)
}
}
}
}
+
+ if(!hasInitCurrentPipeline) {
+ for(pipeHandler in pipelineHandlers) {
+ pipeHandler.init();
+ }
+
+ logger.info("Initialized pipeline $currentPipelineName")
+
+ hasInitCurrentPipeline = true
+ }
}
if(!isActive) {
@@ -305,6 +358,8 @@ class PipelineManager(var eocvSim: EOCVSim) {
updateExceptionTracker(ex)
}
}
+
+ pipelineStatisticsCalculator.endFrame()
}
runBlocking {
@@ -394,10 +449,28 @@ class PipelineManager(var eocvSim: EOCVSim) {
}
}
+ fun subscribePipelineHandler(handler: PipelineHandler) {
+ pipelineHandlers.add(handler)
+ }
+
+ fun addInstantiator(instantiatorFor: Class<*>, instantiator: PipelineInstantiator) {
+ pipelineInstantiators.put(instantiatorFor, instantiator)
+ }
+
+ fun getInstantiatorFor(clazz: Class<*>): PipelineInstantiator? {
+ for((instantiatorFor, instantiator) in pipelineInstantiators) {
+ if(ReflectUtil.hasSuperclass(clazz, instantiatorFor)) {
+ return instantiator
+ }
+ }
+
+ return null
+ }
+
@Suppress("UNCHECKED_CAST")
@JvmOverloads fun addPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) {
try {
- pipelines.add(PipelineData(source, C as Class))
+ pipelines.add(PipelineData(source, C))
} catch (ex: Exception) {
logger.warn("Error while adding pipeline class", ex)
updateExceptionTracker(ex)
@@ -458,9 +531,22 @@ class PipelineManager(var eocvSim: EOCVSim) {
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun forceChangePipeline(index: Int?,
- applyLatestSnapshot: Boolean = false,
+ applyLatestSnapshot: Boolean = applyLatestSnapshotOnChange,
applyStaticSnapshot: Boolean = false) {
- if(index == null) return
+ if(index == null) {
+ previousPipelineIndex = currentPipelineIndex
+
+ currentPipeline = null
+ currentPipelineName = ""
+ currentPipelineContext = null
+ currentPipelineData = null
+ currentPipelineIndex = -1
+
+ onPipelineChange.run()
+ logger.info("Set to null pipeline")
+
+ return
+ }
captureSnapshot()
@@ -470,7 +556,9 @@ class PipelineManager(var eocvSim: EOCVSim) {
logger.info("Changing to pipeline ${pipelineClass.name}")
- var constructor: Constructor<*>
+ debugLogCalled("forceChangePipeline")
+
+ val instantiator = getInstantiatorFor(pipelineClass)
try {
nextTelemetry = TelemetryImpl().apply {
@@ -478,13 +566,8 @@ class PipelineManager(var eocvSim: EOCVSim) {
addTransmissionReceiver(eocvSim.visualizer.telemetryPanel)
}
- try { //instantiate pipeline if it has a constructor of a telemetry parameter
- constructor = pipelineClass.getConstructor(Telemetry::class.java)
- nextPipeline = constructor.newInstance(nextTelemetry) as OpenCvPipeline
- } catch (ex: NoSuchMethodException) { //instantiating with a constructor of no params
- constructor = pipelineClass.getConstructor()
- nextPipeline = constructor.newInstance() as OpenCvPipeline
- }
+ nextPipeline = instantiator?.instantiate(pipelineClass, nextTelemetry)
+ ?: throw RuntimeException("No instantiator found for pipeline class ${pipelineClass.name}")
logger.info("Instantiated pipeline class ${pipelineClass.name}")
} catch (ex: NoSuchMethodException) {
@@ -507,12 +590,16 @@ class PipelineManager(var eocvSim: EOCVSim) {
}
previousPipelineIndex = currentPipelineIndex
+ previousPipeline = currentPipeline
currentPipeline = nextPipeline
currentPipelineData = pipelines[index]
currentTelemetry = nextTelemetry
currentPipelineIndex = index
currentPipelineName = currentPipeline!!.javaClass.simpleName
+ currentTunerTarget = instantiator.variableTunerTargetObject(currentPipeline!!)
+
+ currentTelemetry?.update() // clear telemetry
val snap = PipelineSnapshot(currentPipeline!!, snapshotFieldFilter)
@@ -535,7 +622,7 @@ class PipelineManager(var eocvSim: EOCVSim) {
setPaused(false)
//if pause on images option is turned on by user
- if (eocvSim.configManager.config.pauseOnImages) {
+ if (eocvSim.configManager.config.pauseOnImages && pauseOnImages) {
//pause next frame if current selected input source is an image
eocvSim.inputSourceManager.pauseIfImageTwoFrames()
}
@@ -558,7 +645,11 @@ class PipelineManager(var eocvSim: EOCVSim) {
}
}
- fun requestForceChangePipeline(index: Int) = onUpdate.doOnce { forceChangePipeline(index) }
+ fun requestForceChangePipeline(index: Int) {
+ debugLogCalled("requestForceChangePipeline")
+
+ onUpdate.doOnce { forceChangePipeline(index) }
+ }
fun applyLatestSnapshot() {
if(currentPipeline != null && latestSnapshot != null) {
@@ -645,7 +736,29 @@ class PipelineManager(var eocvSim: EOCVSim) {
eocvSim.onMainUpdate.doOnce { setPaused(paused, pauseReason) }
}
- fun refreshGuiPipelineList() = eocvSim.visualizer.pipelineSelectorPanel.updatePipelinesList()
+ fun refreshGuiPipelineList() {
+ eocvSim.visualizer.pipelineOpModeSwitchablePanel.updateSelectorLists()
+ }
+
+ fun reloadPipelineByName() {
+ for((i, pipeline) in pipelines.withIndex()) {
+ if(pipeline.clazz.name == currentPipelineData?.clazz?.name && pipeline.source == currentPipelineData?.source) {
+ forceChangePipeline(i, true)
+ return
+ }
+ }
+
+ forceChangePipeline(0) // default pipeline
+ }
+
+ private fun debugLogCalled(name: String) {
+ val builder = StringBuilder()
+ for (s in Thread.currentThread().stackTrace) {
+ builder.appendLine(s.toString())
+ }
+
+ logger.debug("$name called in: {}", builder.toString().trim())
+ }
}
@@ -685,6 +798,6 @@ enum class PipelineFps(val fps: Int, val coolName: String) {
}
}
-data class PipelineData(val source: PipelineSource, val clazz: Class)
+data class PipelineData(val source: PipelineSource, val clazz: Class<*>)
enum class PipelineSource { CLASSPATH, COMPILED_ON_RUNTIME }
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt
index d317ced3..3295911b 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt
@@ -23,6 +23,7 @@
package com.github.serivesmejia.eocvsim.pipeline.compiler
+import com.github.serivesmejia.eocvsim.Build
import com.github.serivesmejia.eocvsim.gui.DialogFactory
import com.github.serivesmejia.eocvsim.gui.dialog.Output
import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
@@ -31,6 +32,7 @@ import com.github.serivesmejia.eocvsim.util.StrUtil
import com.github.serivesmejia.eocvsim.util.SysUtil
import com.github.serivesmejia.eocvsim.util.event.EventHandler
import com.github.serivesmejia.eocvsim.util.loggerForThis
+import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfigLoader
import com.github.serivesmejia.eocvsim.workspace.util.template.DefaultWorkspaceTemplate
import com.qualcomm.robotcore.util.ElapsedTime
import kotlinx.coroutines.*
@@ -40,10 +42,21 @@ import java.io.File
class CompiledPipelineManager(private val pipelineManager: PipelineManager) {
companion object {
+ val logger by loggerForThis()
+
val DEF_WORKSPACE_FOLDER = File(SysUtil.getEOCVSimFolder(), File.separator + "default_workspace").apply {
if(!exists()) {
mkdir()
DefaultWorkspaceTemplate.extractToIfEmpty(this)
+ } else {
+ val loader = WorkspaceConfigLoader(this)
+ val config = loader.loadWorkspaceConfig()
+
+ if(config?.eocvSimVersion != Build.standardVersionString) {
+ logger.info("Replacing old default workspace with latest one (version mismatch)")
+ SysUtil.deleteFilesUnder(this)
+ DefaultWorkspaceTemplate.extractTo(this)
+ }
}
}
@@ -125,12 +138,6 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) {
currentPipelineClassLoader = null
val messageEnd = "(took $timeElapsed seconds)\n\n${result.message}".trim()
- val pipelineSelectorPanel = pipelineManager.eocvSim.visualizer.pipelineSelectorPanel
- val beforeAllowSwitching = pipelineSelectorPanel?.allowPipelineSwitching
-
- if(fixSelectedPipeline)
- pipelineSelectorPanel?.allowPipelineSwitching = false
-
pipelineManager.requestRemoveAllPipelinesFrom(
PipelineSource.COMPILED_ON_RUNTIME,
refreshGuiPipelineList = false,
@@ -156,22 +163,9 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) {
}
}
- val beforePipeline = pipelineManager.currentPipelineData
-
pipelineManager.onUpdate.doOnce {
pipelineManager.refreshGuiPipelineList()
-
- if(fixSelectedPipeline) {
- if(beforePipeline != null) {
- val pipeline = pipelineManager.getIndexOf(beforePipeline.clazz, beforePipeline.source)
-
- pipelineManager.forceChangePipeline(pipeline, true)
- } else {
- pipelineManager.changePipeline(0) //default pipeline
- }
-
- pipelineSelectorPanel?.allowPipelineSwitching = beforeAllowSwitching!!
- }
+ pipelineManager.reloadPipelineByName()
}
if(result.status == PipelineCompileStatus.SUCCESS) {
@@ -238,12 +232,12 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) {
fun loadFromPipelinesJar() {
if(!PIPELINES_OUTPUT_JAR.exists()) return
- logger.trace("Looking for pipelines in jar file $PIPELINES_OUTPUT_JAR")
+ logger.trace("Looking for pipelines in jar file {}", PIPELINES_OUTPUT_JAR)
try {
currentPipelineClassLoader = PipelineClassLoader(PIPELINES_OUTPUT_JAR)
- val pipelines = mutableListOf>()
+ val pipelines = mutableListOf>()
for(pipelineClass in currentPipelineClassLoader!!.pipelineClasses) {
pipelines.add(pipelineClass)
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt
index b271bf46..c4ed1d20 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt
@@ -24,11 +24,13 @@
package com.github.serivesmejia.eocvsim.pipeline.compiler
import com.github.serivesmejia.eocvsim.util.ClasspathScan
-import com.github.serivesmejia.eocvsim.util.ReflectUtil
import com.github.serivesmejia.eocvsim.util.SysUtil
import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd
import org.openftc.easyopencv.OpenCvPipeline
-import java.io.*
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@@ -38,7 +40,7 @@ class PipelineClassLoader(pipelinesJar: File) : ClassLoader() {
private val zipFile = ZipFile(pipelinesJar)
private val loadedClasses = mutableMapOf>()
- var pipelineClasses: List>
+ var pipelineClasses: List>
private set
init {
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt
index df8676dd..b25fd31c 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt
@@ -26,10 +26,8 @@ package com.github.serivesmejia.eocvsim.pipeline.compiler
import com.github.serivesmejia.eocvsim.util.*
import com.github.serivesmejia.eocvsim.util.compiler.JarPacker
import com.github.serivesmejia.eocvsim.util.compiler.compiler
-import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler
import java.io.File
import java.io.PrintWriter
-import java.lang.reflect.Field
import java.nio.charset.Charset
import java.util.*
import javax.tools.*
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt
new file mode 100644
index 00000000..fa6d5824
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/PipelineHandler.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.pipeline.handler
+
+import com.github.serivesmejia.eocvsim.input.InputSource
+import org.firstinspires.ftc.robotcore.external.Telemetry
+import org.openftc.easyopencv.OpenCvPipeline
+
+interface PipelineHandler {
+
+ fun preInit()
+
+ fun init()
+
+ fun processFrame(currentInputSource: InputSource?)
+
+ fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry)
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt
new file mode 100644
index 00000000..568c32b2
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/handler/SpecificPipelineHandler.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.pipeline.handler
+
+import org.firstinspires.ftc.robotcore.external.Telemetry
+import org.openftc.easyopencv.OpenCvPipeline
+
+abstract class SpecificPipelineHandler(
+ val typeChecker: (OpenCvPipeline) -> Boolean
+) : PipelineHandler {
+
+ var pipeline: P? = null
+ private set
+
+ var telemetry: Telemetry? = null
+ private set
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) {
+ if(typeChecker(newPipeline)) {
+ this.pipeline = newPipeline as P
+ this.telemetry = telemetry
+ } else {
+ this.pipeline = null
+ this.telemetry = null // "don't get paid enough to handle this shit"
+ // - OpModePipelineHandler, probably
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt
new file mode 100644
index 00000000..f4b74ec5
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/DefaultPipelineInstantiator.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.pipeline.instantiator
+
+import org.firstinspires.ftc.robotcore.external.Telemetry
+import org.openftc.easyopencv.OpenCvPipeline
+
+object DefaultPipelineInstantiator : PipelineInstantiator {
+
+ override fun instantiate(clazz: Class<*>, telemetry: Telemetry) = try {
+ //instantiate pipeline if it has a constructor of a telemetry parameter
+ val constructor = clazz.getConstructor(Telemetry::class.java)
+ constructor.newInstance(telemetry) as OpenCvPipeline
+ } catch (ex: NoSuchMethodException) {
+ //instantiating with a constructor of no params
+ val constructor = clazz.getConstructor()
+ constructor.newInstance() as OpenCvPipeline
+ }
+
+ override fun variableTunerTargetObject(pipeline: OpenCvPipeline) = pipeline
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt
new file mode 100644
index 00000000..c3e30c49
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/PipelineInstantiator.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.pipeline.instantiator
+
+import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
+import org.firstinspires.ftc.robotcore.external.Telemetry
+import org.openftc.easyopencv.OpenCvPipeline
+
+interface PipelineInstantiator {
+
+ fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline
+
+ fun variableTunerTargetObject(pipeline: OpenCvPipeline): Any
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt
new file mode 100644
index 00000000..d42134c8
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorInstantiator.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.pipeline.instantiator.processor
+
+import com.github.serivesmejia.eocvsim.pipeline.PipelineManager
+import com.github.serivesmejia.eocvsim.pipeline.instantiator.PipelineInstantiator
+import com.github.serivesmejia.eocvsim.util.ReflectUtil
+import org.firstinspires.ftc.robotcore.external.Telemetry
+import org.firstinspires.ftc.vision.VisionProcessor
+import org.openftc.easyopencv.OpenCvPipeline
+
+object ProcessorInstantiator : PipelineInstantiator {
+ override fun instantiate(clazz: Class<*>, telemetry: Telemetry): OpenCvPipeline {
+ if(!ReflectUtil.hasSuperclass(clazz, VisionProcessor::class.java))
+ throw IllegalArgumentException("Class $clazz does not extend VisionProcessor")
+
+ val processor = try {
+ //instantiate pipeline if it has a constructor of a telemetry parameter
+ val constructor = clazz.getConstructor(Telemetry::class.java)
+ constructor.newInstance(telemetry) as VisionProcessor
+ } catch (ex: NoSuchMethodException) {
+ //instantiating with a constructor of no params
+ val constructor = clazz.getConstructor()
+ constructor.newInstance() as VisionProcessor
+ }
+
+ return ProcessorPipeline(processor)
+ }
+
+ override fun variableTunerTargetObject(pipeline: OpenCvPipeline): VisionProcessor =
+ (pipeline as ProcessorPipeline).processor
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java
new file mode 100644
index 00000000..d56cf1a6
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/instantiator/processor/ProcessorPipeline.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package com.github.serivesmejia.eocvsim.pipeline.instantiator.processor;
+
+import android.graphics.Canvas;
+import com.qualcomm.robotcore.eventloop.opmode.Disabled;
+import org.firstinspires.ftc.vision.VisionProcessor;
+import org.opencv.core.Mat;
+import org.openftc.easyopencv.TimestampedOpenCvPipeline;
+
+@Disabled
+class ProcessorPipeline extends TimestampedOpenCvPipeline {
+
+ VisionProcessor processor;
+
+ public ProcessorPipeline(VisionProcessor processor) {
+ this.processor = processor;
+ }
+
+ @Override
+ public void init(Mat mat) {
+ processor.init(mat.width(), mat.height(), null);
+ }
+
+ @Override
+ public Mat processFrame(Mat input, long captureTimeNanos) {
+ requestViewportDrawHook(processor.processFrame(input, captureTimeNanos));
+ return input;
+ }
+
+ @Override
+ public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) {
+ processor.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, userContext);
+ }
+
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt
index 953232ed..788c919a 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt
@@ -136,12 +136,8 @@ class PipelineExceptionTracker(private val pipelineManager: PipelineManager) {
val expiresIn = millisExceptionExpire - (System.currentTimeMillis() - data.millisThrown)
val expiresInSecs = String.format("%.1f", expiresIn.toDouble() / 1000.0)
- val shortStacktrace = StrUtil.cutStringBy(
- data.stacktrace, "\n", cutStacktraceLines
- ).trim()
-
messageBuilder
- .appendLine("> $shortStacktrace")
+ .appendLine("> ${data.stacktrace}")
.appendLine()
.appendLine("! It has been thrown ${data.count} times, and will expire in $expiresInSecs seconds !")
.appendLine()
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java
index dfecbe76..c1c707d2 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java
@@ -30,15 +30,13 @@
import org.openftc.easyopencv.OpenCvPipeline;
import java.lang.reflect.Field;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
public abstract class TunableField {
protected Field reflectionField;
protected TunableFieldPanel fieldPanel;
- protected OpenCvPipeline pipeline;
+ protected Object target;
protected AllowMode allowMode;
protected EOCVSim eocvSim;
@@ -51,17 +49,17 @@ public abstract class TunableField {
private TunableFieldPanel.Mode recommendedMode = null;
- public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException {
+ public TunableField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException {
this.reflectionField = reflectionField;
- this.pipeline = instance;
+ this.target = target;
this.allowMode = allowMode;
this.eocvSim = eocvSim;
- initialFieldValue = reflectionField.get(instance);
+ initialFieldValue = reflectionField.get(target);
}
- public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- this(instance, reflectionField, eocvSim, AllowMode.TEXT);
+ public TunableField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ this(target, reflectionField, eocvSim, AllowMode.TEXT);
}
public abstract void init();
@@ -72,7 +70,7 @@ public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocv
public void setPipelineFieldValue(T newValue) throws IllegalAccessException {
if (hasChanged()) { //execute if value is not the same to save resources
- reflectionField.set(pipeline, newValue);
+ reflectionField.set(target, newValue);
onValueChange.run();
}
}
@@ -127,6 +125,10 @@ public final String getFieldName() {
return reflectionField.getName();
}
+ public final String getFieldTypeName() {
+ return reflectionField.getType().getSimpleName();
+ }
+
public final AllowMode getAllowMode() {
return allowMode;
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java
index 3c3fbfa7..089fbb2b 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java
@@ -84,7 +84,7 @@ public void init() {
}
if (eocvSim.pipelineManager.getCurrentPipeline() != null) {
- addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline());
+ addFieldsFrom(eocvSim.pipelineManager.getCurrentTunerTarget());
eocvSim.visualizer.updateTunerFields(createTunableFieldPanels());
for(TunableField field : fields.toArray(new TunableField[0])) {
@@ -145,11 +145,10 @@ public Class extends TunableField> getTunableFieldOf(Field field) {
return tunableFieldClass;
}
- public void addFieldsFrom(OpenCvPipeline pipeline) {
+ public void addFieldsFrom(Object target) {
+ if (target == null) return;
- if (pipeline == null) return;
-
- Field[] fields = pipeline.getClass().getFields();
+ Field[] fields = target.getClass().getFields();
for (Field field : fields) {
Class extends TunableField> tunableFieldClass = getTunableFieldOf(field);
@@ -161,8 +160,8 @@ public void addFieldsFrom(OpenCvPipeline pipeline) {
//now, lets do some more reflection to instantiate this TunableField
//and add it to the list...
try {
- Constructor extends TunableField> constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, Field.class, EOCVSim.class);
- this.fields.add(constructor.newInstance(pipeline, field, eocvSim));
+ Constructor extends TunableField> constructor = tunableFieldClass.getConstructor(Object.class, Field.class, EOCVSim.class);
+ this.fields.add(constructor.newInstance(target, field, eocvSim));
} catch(InvocationTargetException e) {
if(e.getCause() instanceof CancelTunableFieldAddingException) {
String message = e.getCause().getMessage();
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java
index 289e49a3..8e6b3380 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java
@@ -38,15 +38,13 @@ public class BooleanField extends TunableField {
boolean lastVal;
volatile boolean hasChanged = false;
- public BooleanField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
-
- super(instance, reflectionField, eocvSim, AllowMode.TEXT);
+ public BooleanField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.TEXT);
setGuiFieldAmount(0);
setGuiComboBoxAmount(1);
value = (boolean) initialFieldValue;
-
}
@Override
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt
index 42f60e99..eb592693 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt
@@ -4,14 +4,12 @@ import com.github.serivesmejia.eocvsim.EOCVSim
import com.github.serivesmejia.eocvsim.tuner.TunableField
import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor
import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField
-import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableFieldAcceptor
-import org.openftc.easyopencv.OpenCvPipeline
import java.lang.reflect.Field
@RegisterTunableField
-class EnumField(private val instance: OpenCvPipeline,
+class EnumField(target: Any,
reflectionField: Field,
- eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) {
+ eocvSim: EOCVSim) : TunableField>(target, reflectionField, eocvSim, AllowMode.TEXT) {
val values = reflectionField.type.enumConstants
@@ -45,7 +43,7 @@ class EnumField(private val instance: OpenCvPipeline,
override fun setGuiFieldValue(index: Int, newValue: String) {
currentValue = java.lang.Enum.valueOf(initialValue::class.java, newValue)
- reflectionField.set(instance, currentValue)
+ reflectionField.set(target, currentValue)
}
override fun getValue() = currentValue
@@ -56,7 +54,7 @@ class EnumField(private val instance: OpenCvPipeline,
return values
}
- override fun hasChanged() = reflectionField.get(instance) != beforeValue
+ override fun hasChanged() = reflectionField.get(target) != beforeValue
class EnumFieldAcceptor : TunableFieldAcceptor {
override fun accept(clazz: Class<*>) = clazz.isEnum
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java
index cdca560a..2b5af34e 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java
@@ -36,8 +36,8 @@ public class NumericField extends TunableField {
protected volatile boolean hasChanged = false;
- public NumericField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, allowMode);
+ public NumericField(Object target, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, allowMode);
}
@Override
@@ -50,7 +50,7 @@ public void update() {
if (value == null) return;
try {
- value = (T) reflectionField.get(pipeline);
+ value = (T) reflectionField.get(target);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java
index 886ee3a5..d80895b1 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java
@@ -40,8 +40,8 @@ public class StringField extends TunableField {
volatile boolean hasChanged = false;
- public StringField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, AllowMode.TEXT);
+ public StringField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.TEXT);
if(initialFieldValue != null) {
value = (String) initialFieldValue;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java
index 6972c82e..7b7a6734 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java
@@ -40,9 +40,8 @@ public class PointField extends TunableField {
volatile boolean hasChanged = false;
- public PointField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
-
- super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
+ public PointField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
if(initialFieldValue != null) {
Point p = (Point) initialFieldValue;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt
index f8dc89d4..b83cf1de 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt
@@ -31,8 +31,8 @@ import org.openftc.easyopencv.OpenCvPipeline
import java.lang.reflect.Field
@RegisterTunableField
-class RectField(instance: OpenCvPipeline, reflectionField: Field, eocvSim: EOCVSim) :
- TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) {
+class RectField(target: Any, reflectionField: Field, eocvSim: EOCVSim) :
+ TunableField(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) {
private var rect = arrayOf(0.0, 0.0, 0.0, 0.0)
private var lastRect = arrayOf(0.0, 0.0, 0.0, 0.0)
@@ -56,7 +56,7 @@ class RectField(instance: OpenCvPipeline, reflectionField: Field, eocvSim: EOCVS
override fun update() {
if(hasChanged()){
- initialRect = reflectionField.get(pipeline) as Rect
+ initialRect = reflectionField.get(target) as Rect
rect[0] = initialRect.x.toDouble()
rect[1] = initialRect.y.toDouble()
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java
index 6df85a78..fa79db64 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java
@@ -43,8 +43,8 @@ public class ScalarField extends TunableField {
volatile boolean hasChanged = false;
- public ScalarField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
+ public ScalarField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
if(initialFieldValue == null) {
scalar = new Scalar(0, 0, 0);
@@ -63,7 +63,7 @@ public void init() { }
@Override
public void update() {
try {
- scalar = (Scalar) reflectionField.get(pipeline);
+ scalar = (Scalar) reflectionField.get(target);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java
index 32dd0ef2..572bedbd 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java
@@ -35,8 +35,8 @@ public class DoubleField extends NumericField {
private double beforeValue;
- public DoubleField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
+ public DoubleField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
value = (double) initialFieldValue;
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java
index ec788b74..321e01dd 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java
@@ -35,8 +35,8 @@ public class FloatField extends NumericField {
protected float beforeValue;
- public FloatField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
+ public FloatField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL);
value = (float) initialFieldValue;
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java
index 99b8aa01..08ff9bf2 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java
@@ -35,8 +35,8 @@ public class IntegerField extends NumericField {
protected int beforeValue;
- public IntegerField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS);
+ public IntegerField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS);
value = (int) initialFieldValue;
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java
index 45fdff7f..6645a505 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java
@@ -35,8 +35,8 @@ public class LongField extends NumericField {
private long beforeValue;
- public LongField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
- super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS);
+ public LongField(Object target, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException {
+ super(target, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS);
value = (long) initialFieldValue;
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt
index 4115bcff..68eab3a6 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt
@@ -27,9 +27,12 @@ import com.github.serivesmejia.eocvsim.tuner.TunableField
import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor
import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField
import com.qualcomm.robotcore.eventloop.opmode.Disabled
+import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode
+import com.qualcomm.robotcore.eventloop.opmode.OpMode
import com.qualcomm.robotcore.util.ElapsedTime
import io.github.classgraph.ClassGraph
import kotlinx.coroutines.*
+import org.firstinspires.ftc.vision.VisionProcessor
import org.openftc.easyopencv.OpenCvPipeline
class ClasspathScan {
@@ -43,7 +46,6 @@ class ClasspathScan {
"io.github.classgraph",
"io.github.deltacv",
"com.github.serivesmejia.eocvsim.pipeline",
- "org.openftc",
"org.lwjgl",
"org.apache",
"org.codehaus",
@@ -59,11 +61,11 @@ class ClasspathScan {
private lateinit var scanResultJob: Job
@Suppress("UNCHECKED_CAST")
- fun scan(jarFile: String? = null, classLoader: ClassLoader? = null): ScanResult {
+ fun scan(jarFile: String? = null, classLoader: ClassLoader? = null, addProcessorsAsPipelines: Boolean = true): ScanResult {
val timer = ElapsedTime()
val classGraph = ClassGraph()
.enableClassInfo()
- //.verbose()
+ // .verbose()
.enableAnnotationInfo()
.rejectPackages(*ignoredPackages)
@@ -74,30 +76,76 @@ class ClasspathScan {
logger.info("Starting to scan classpath...")
}
+ if(classLoader != null) {
+ classGraph.overrideClassLoaders(classLoader)
+ }
+
val scanResult = classGraph.scan()
logger.info("ClassGraph finished scanning (took ${timer.seconds()}s)")
val tunableFieldClassesInfo = scanResult.getClassesWithAnnotation(RegisterTunableField::class.java.name)
- val pipelineClassesInfo = scanResult.getSubclasses(OpenCvPipeline::class.java.name)
- val pipelineClasses = mutableListOf>()
+ val pipelineClasses = mutableListOf>()
- for(pipelineClassInfo in pipelineClassesInfo) {
- val clazz = if(classLoader != null) {
- classLoader.loadClass(pipelineClassInfo.name)
- } else Class.forName(pipelineClassInfo.name)
-
- if(ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) {
- if(clazz.isAnnotationPresent(Disabled::class.java)) {
- logger.info("Found @Disabled pipeline ${clazz.typeName}")
- } else {
- logger.info("Found pipeline ${clazz.typeName}")
- pipelineClasses.add(clazz as Class)
+ // i...don't even know how to name this, sorry, future readers
+ // but classgraph for some reason does not have a recursive search for subclasses...
+ fun searchPipelinesOfSuperclass(superclass: String) {
+ logger.trace("searchPipelinesOfSuperclass: {}", superclass)
+
+ val superclassClazz = if(classLoader != null) {
+ classLoader.loadClass(superclass)
+ } else Class.forName(superclass)
+
+ val pipelineClassesInfo = if(superclassClazz.isInterface)
+ scanResult.getClassesImplementing(superclass)
+ else scanResult.getSubclasses(superclass)
+
+ for(pipelineClassInfo in pipelineClassesInfo) {
+ logger.trace("pipelineClassInfo: {}", pipelineClassInfo.name)
+
+ for(pipelineSubclassInfo in pipelineClassInfo.subclasses) {
+ searchPipelinesOfSuperclass(pipelineSubclassInfo.name) // naming is my passion
+ }
+
+ if(pipelineClassInfo.isAbstract || pipelineClassInfo.isInterface) {
+ continue // nope'd outta here
+ }
+
+ val clazz = if(classLoader != null) {
+ classLoader.loadClass(pipelineClassInfo.name)
+ } else Class.forName(pipelineClassInfo.name)
+
+ logger.trace("class {} super {}", clazz.typeName, clazz.superclass.typeName)
+
+ if(!pipelineClasses.contains(clazz) && ReflectUtil.hasSuperclass(clazz, superclassClazz)) {
+ if(clazz.isAnnotationPresent(Disabled::class.java)) {
+ logger.info("Found @Disabled pipeline ${clazz.typeName}")
+ } else {
+ logger.info("Found pipeline ${clazz.typeName}")
+ pipelineClasses.add(clazz)
+ }
}
}
}
+ // start recursive hell
+ searchPipelinesOfSuperclass(OpenCvPipeline::class.java.name)
+
+ if(jarFile != null) {
+ // Since we removed EOCV-Sim from the scan classpath,
+ // ClassGraph does not know that OpMode and LinearOpMode
+ // are subclasses of OpenCvPipeline, so we have to scan them
+ // manually...
+ searchPipelinesOfSuperclass(OpMode::class.java.name)
+ searchPipelinesOfSuperclass(LinearOpMode::class.java.name)
+ }
+
+ if(addProcessorsAsPipelines) {
+ logger.info("Searching for VisionProcessors...")
+ searchPipelinesOfSuperclass(VisionProcessor::class.java.name)
+ }
+
logger.info("Found ${pipelineClasses.size} pipelines")
val tunableFieldClasses = mutableListOf>>()
@@ -151,7 +199,7 @@ class ClasspathScan {
}
data class ScanResult(
- val pipelineClasses: Array>,
+ val pipelineClasses: Array>,
val tunableFieldClasses: Array>>,
val tunableFieldAcceptorClasses: Map>, Class>
)
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java
index 07c18ec8..16626f6d 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java
@@ -183,7 +183,6 @@ public static Optional getExtensionByStringHandling(String filename) {
.filter(f -> f.contains("."))
.map(f -> f.substring(filename.lastIndexOf(".") + 1));
}
-
public static List filesUnder(File parent, Predicate predicate) {
ArrayList result = new ArrayList<>();
@@ -244,7 +243,6 @@ public static void deleteFilesUnder(File parent, Predicate predicate) {
public static void deleteFilesUnder(File parent) {
deleteFilesUnder(parent, null);
}
-
public static boolean migrateFile(File oldFile, File newFile) {
if(newFile.exists() || !oldFile.exists()) return false;
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt
index 16bd52ea..4eee1641 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt
@@ -30,10 +30,7 @@ import com.github.serivesmejia.eocvsim.util.SysUtil
import com.github.serivesmejia.eocvsim.util.extension.plus
import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder
import com.github.serivesmejia.eocvsim.util.loggerForThis
-import java.io.BufferedWriter
import java.io.File
-import java.io.FileWriter
-import java.nio.CharBuffer
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt
index dd995bb4..82926c38 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt
@@ -2,6 +2,7 @@ package com.github.serivesmejia.eocvsim.util.exception.handling
import com.github.serivesmejia.eocvsim.currentMainThread
import com.github.serivesmejia.eocvsim.util.loggerForThis
+import javax.swing.SwingUtilities
import kotlin.system.exitProcess
class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExceptionHandler {
@@ -33,7 +34,7 @@ class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExc
//Exit if uncaught exception happened in the main thread
//since we would be basically in a deadlock state if that happened
//or if we have a lotta uncaught exceptions.
- if(t == currentMainThread || e !is Exception || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) {
+ if(t == currentMainThread || SwingUtilities.isEventDispatchThread() || e !is Exception || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) {
CrashReport(e).saveCrashReport()
logger.warn("If this error persists, open an issue on EOCV-Sim's GitHub attaching the crash report file.")
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt
index c4f8e8a5..a5108883 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt
@@ -26,5 +26,5 @@ package com.github.serivesmejia.eocvsim.util.extension
import java.io.File
operator fun File.plus(str: String): File {
- return File(this.absolutePath + str)
-}
\ No newline at end of file
+ return File(this.absolutePath, str)
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt
index 26651a40..8a690357 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt
@@ -28,7 +28,6 @@ import com.github.serivesmejia.eocvsim.util.SysUtil
import com.github.serivesmejia.eocvsim.util.loggerOf
import org.slf4j.Logger
import java.io.File
-import java.util.*
class FileWatcher(private val watchingDirectories: List,
watchingFileExtensions: List?,
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java
index 1423855e..8fb46038 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java
@@ -29,8 +29,9 @@ public class WorkspaceConfig {
public String sourcesPath = ".";
public String resourcesPath = ".";
-
public ArrayList excludedPaths = new ArrayList<>();
public ArrayList excludedFileExtensions = new ArrayList<>();
+ public String eocvSimVersion = "";
+
}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt
index 05ac32a7..71dbf094 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt
@@ -1,6 +1,8 @@
package com.github.serivesmejia.eocvsim.workspace.config
+import com.github.serivesmejia.eocvsim.Build
import com.github.serivesmejia.eocvsim.util.SysUtil
+import com.github.serivesmejia.eocvsim.util.loggerForThis
import com.google.gson.GsonBuilder
import java.io.File
@@ -12,6 +14,8 @@ class WorkspaceConfigLoader(var workspaceFile: File) {
val workspaceConfigFile get() = File(workspaceFile, File.separator + "eocvsim_workspace.json")
+ private val logger by loggerForThis()
+
fun loadWorkspaceConfig(): WorkspaceConfig? {
if(!workspaceConfigFile.exists()) return null
@@ -20,11 +24,13 @@ class WorkspaceConfigLoader(var workspaceFile: File) {
return try {
gson.fromJson(configStr, WorkspaceConfig::class.java)
} catch(e: Exception) {
+ logger.error("Failed to load workspace config", e)
null
}
}
fun saveWorkspaceConfig(config: WorkspaceConfig) {
+ config.eocvSimVersion = Build.standardVersionString
val configStr = gson.toJson(config)
SysUtil.saveFileStr(workspaceConfigFile, configStr)
}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt
index b1009550..48037d28 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt
+++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt
@@ -25,7 +25,6 @@ package com.github.serivesmejia.eocvsim.workspace.util.template
import com.github.serivesmejia.eocvsim.util.SysUtil
import com.github.serivesmejia.eocvsim.util.loggerForThis
-import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher
import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate
import net.lingala.zip4j.ZipFile
import java.io.File
diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt
new file mode 100644
index 00000000..057c6b2a
--- /dev/null
+++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpModePipelineHandler.kt
@@ -0,0 +1,61 @@
+package com.qualcomm.robotcore.eventloop.opmode
+
+import com.github.serivesmejia.eocvsim.EOCVSim
+import com.github.serivesmejia.eocvsim.input.InputSource
+import com.github.serivesmejia.eocvsim.input.InputSourceManager
+import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler
+import com.github.serivesmejia.eocvsim.util.event.EventHandler
+import com.qualcomm.robotcore.hardware.HardwareMap
+import io.github.deltacv.eocvsim.input.VisionInputSourceHander
+import io.github.deltacv.vision.external.source.ThreadSourceHander
+import org.firstinspires.ftc.robotcore.external.Telemetry
+import org.openftc.easyopencv.OpenCvPipeline
+import org.openftc.easyopencv.OpenCvViewport
+
+enum class OpModeType { AUTONOMOUS, TELEOP }
+
+class OpModePipelineHandler(val inputSourceManager: InputSourceManager, private val viewport: OpenCvViewport) : SpecificPipelineHandler(
+ { it is OpMode }
+) {
+
+ private val onStop = EventHandler("OpModePipelineHandler-onStop")
+
+ override fun preInit() {
+ if(pipeline == null) return
+
+ inputSourceManager.setInputSource(inputSourceManager.defaultInputSource)
+ ThreadSourceHander.register(VisionInputSourceHander(pipeline!!.notifier, viewport))
+
+ pipeline?.telemetry = telemetry
+ pipeline?.hardwareMap = HardwareMap()
+ }
+
+ override fun init() { }
+
+ override fun processFrame(currentInputSource: InputSource?) {
+ }
+
+ override fun onChange(beforePipeline: OpenCvPipeline?, newPipeline: OpenCvPipeline, telemetry: Telemetry) {
+ if(beforePipeline is OpMode) {
+ beforePipeline.forceStop()
+ onStop.run()
+ }
+
+ super.onChange(beforePipeline, newPipeline, telemetry)
+ }
+
+}
+
+val Class<*>.opModeType get() = when {
+ this.autonomousAnnotation != null -> OpModeType.AUTONOMOUS
+ this.teleopAnnotation != null -> OpModeType.TELEOP
+ else -> null
+}
+
+val OpMode.opModeType get() = this.javaClass.opModeType
+
+val Class<*>.autonomousAnnotation get() = this.getAnnotation(Autonomous::class.java)
+val Class<*>.teleopAnnotation get() = this.getAnnotation(TeleOp::class.java)
+
+val OpMode.autonomousAnnotation get() = this.javaClass.autonomousAnnotation
+val OpMode.teleopAnnotation get() = this.javaClass.teleopAnnotation
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt
new file mode 100644
index 00000000..32255731
--- /dev/null
+++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSource.kt
@@ -0,0 +1,48 @@
+package io.github.deltacv.eocvsim.input
+
+import com.github.serivesmejia.eocvsim.input.InputSource
+import com.github.serivesmejia.eocvsim.util.loggerForThis
+import io.github.deltacv.vision.external.source.VisionSourceBase
+import io.github.deltacv.vision.external.util.Timestamped
+import org.opencv.core.Mat
+import org.opencv.core.Size
+
+class VisionInputSource(
+ private val inputSource: InputSource
+) : VisionSourceBase() {
+
+ val logger by loggerForThis()
+
+ override fun init(): Int {
+ return 0
+ }
+
+ override fun close(): Boolean {
+ inputSource.close()
+ inputSource.reset()
+ return true
+ }
+
+ override fun startSource(size: Size?): Boolean {
+ inputSource.setSize(size)
+ inputSource.init()
+ return true
+ }
+
+ override fun stopSource(): Boolean {
+ inputSource.close()
+ return true;
+ }
+
+ private val emptyMat = Mat()
+
+ override fun pullFrame(): Timestamped {
+ try {
+ val frame = inputSource.update();
+ return Timestamped(frame, inputSource.captureTimeNanos)
+ } catch(e: Exception) {
+ return Timestamped(emptyMat, 0)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt
new file mode 100644
index 00000000..5b553af0
--- /dev/null
+++ b/EOCV-Sim/src/main/java/io/github/deltacv/eocvsim/input/VisionInputSourceHander.kt
@@ -0,0 +1,72 @@
+package io.github.deltacv.eocvsim.input
+
+import com.github.serivesmejia.eocvsim.input.source.CameraSource
+import com.github.serivesmejia.eocvsim.input.source.ImageSource
+import com.github.serivesmejia.eocvsim.input.source.VideoSource
+import com.github.serivesmejia.eocvsim.util.event.EventHandler
+import com.qualcomm.robotcore.eventloop.opmode.OpMode
+import io.github.deltacv.vision.external.source.ViewportAndSourceHander
+import io.github.deltacv.vision.external.source.VisionSource
+import io.github.deltacv.vision.external.source.VisionSourceHander
+import io.github.deltacv.vision.internal.opmode.OpModeNotifier
+import io.github.deltacv.vision.internal.opmode.OpModeState
+import org.opencv.core.Mat
+import org.opencv.core.Size
+import org.opencv.videoio.VideoCapture
+import org.openftc.easyopencv.OpenCvViewport
+import java.io.File
+import java.io.IOException
+import java.lang.IllegalArgumentException
+import java.net.URLConnection
+import javax.imageio.ImageIO
+
+class VisionInputSourceHander(val notifier: OpModeNotifier, val viewport: OpenCvViewport) : ViewportAndSourceHander {
+
+ private fun isImage(path: String) = try {
+ ImageIO.read(File(path)) != null
+ } catch(ex: IOException) { false }
+
+ private fun isVideo(path: String): Boolean {
+ val capture = VideoCapture(path)
+ val mat = Mat()
+
+ capture.read(mat)
+
+ val isVideo = !mat.empty()
+
+ capture.release()
+
+ return isVideo
+ }
+
+ override fun hand(name: String): VisionSource {
+ val source = VisionInputSource(if(File(name).exists()) {
+ if(isImage(name)) {
+ ImageSource(name)
+ } else if(isVideo(name)) {
+ VideoSource(name, null)
+ } else throw IllegalArgumentException("File is not an image nor a video")
+ } else {
+ val index = name.toIntOrNull()
+ ?: if(name == "default" || name == "Webcam 1") 0
+ else throw IllegalArgumentException("Unknown source $name")
+
+ CameraSource(index, Size(640.0, 480.0))
+ })
+
+ notifier.onStateChange {
+ when(notifier.state) {
+ OpModeState.STOPPED -> {
+ source.stop()
+ it.removeThis()
+ }
+ else -> {}
+ }
+ }
+
+ return source
+ }
+
+ override fun viewport() = viewport
+
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt b/EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt
new file mode 100644
index 00000000..eaba57f7
--- /dev/null
+++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/ProcessFrameInternalAccessor.kt
@@ -0,0 +1,5 @@
+package org.openftc.easyopencv
+
+import org.opencv.core.Mat
+
+fun OpenCvPipeline.processFrameInternal(frame: Mat): Mat? = processFrameInternal(frame)
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt
index 74248561..70809c18 100644
--- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt
+++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt
@@ -24,13 +24,21 @@
package org.openftc.easyopencv
import com.github.serivesmejia.eocvsim.input.InputSource
+import com.github.serivesmejia.eocvsim.pipeline.handler.PipelineHandler
+import com.github.serivesmejia.eocvsim.pipeline.handler.SpecificPipelineHandler
+import org.firstinspires.ftc.robotcore.external.Telemetry
-class TimestampedPipelineHandler {
+class TimestampedPipelineHandler : SpecificPipelineHandler(
+ { it is TimestampedOpenCvPipeline }
+) {
+ override fun preInit() {
+ }
- fun update(currentPipeline: OpenCvPipeline?, currentInputSource: InputSource?) {
- if(currentPipeline is TimestampedOpenCvPipeline) {
- currentPipeline.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L)
- }
+ override fun init() {
+ pipeline?.setTimestamp(0)
}
+ override fun processFrame(currentInputSource: InputSource?) {
+ pipeline?.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L)
+ }
}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_arrow_dropdown.png b/EOCV-Sim/src/main/resources/images/icon/ico_arrow_dropdown.png
new file mode 100644
index 00000000..0ba2fb50
Binary files /dev/null and b/EOCV-Sim/src/main/resources/images/icon/ico_arrow_dropdown.png differ
diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_flag.png b/EOCV-Sim/src/main/resources/images/icon/ico_flag.png
new file mode 100644
index 00000000..d86becec
Binary files /dev/null and b/EOCV-Sim/src/main/resources/images/icon/ico_flag.png differ
diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_not_started.png b/EOCV-Sim/src/main/resources/images/icon/ico_not_started.png
new file mode 100644
index 00000000..1b39317f
Binary files /dev/null and b/EOCV-Sim/src/main/resources/images/icon/ico_not_started.png differ
diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_play.png b/EOCV-Sim/src/main/resources/images/icon/ico_play.png
new file mode 100644
index 00000000..511257a7
Binary files /dev/null and b/EOCV-Sim/src/main/resources/images/icon/ico_play.png differ
diff --git a/EOCV-Sim/src/main/resources/images/icon/ico_stop.png b/EOCV-Sim/src/main/resources/images/icon/ico_stop.png
new file mode 100644
index 00000000..3a28f72d
Binary files /dev/null and b/EOCV-Sim/src/main/resources/images/icon/ico_stop.png differ
diff --git a/EOCV-Sim/src/main/resources/opensourcelibs.txt b/EOCV-Sim/src/main/resources/opensourcelibs.txt
index c02ffecc..ce70c5a7 100644
--- a/EOCV-Sim/src/main/resources/opensourcelibs.txt
+++ b/EOCV-Sim/src/main/resources/opensourcelibs.txt
@@ -1,11 +1,12 @@
EOCV-Sim and its source code is distributed under the MIT License
OpenCV - Under Apache 2.0 License
-OpenCV for Desktop Java - Under Apache 2.0 License
-FTC SDK - Some source code under the BSD License
+OpenPnP OpenCV - Under Apache 2.0 License
+FTC SDK - Some source code under BSD License
EasyOpenCV - Some source code under MIT License
EOCV-AprilTag-Plugin - Source code under MIT License
webcam-capture - Under MIT License
+Skiko - Under Apache 2.0 License
Gson - Under Apache 2.0 License
ClassGraph - Under MIT License
FlatLaf - Under Apache 2.0 License
diff --git a/EOCV-Sim/src/main/resources/templates/default_workspace.zip b/EOCV-Sim/src/main/resources/templates/default_workspace.zip
index 8685c67f..cb37d808 100644
Binary files a/EOCV-Sim/src/main/resources/templates/default_workspace.zip and b/EOCV-Sim/src/main/resources/templates/default_workspace.zip differ
diff --git a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip
index e85cf7a0..e1d353c6 100644
Binary files a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip and b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip differ
diff --git a/README.md b/README.md
index 294f538c..97a5e142 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ Since OpenCV in Java uses a native library, which is platform specific, the simu
* Windows x86_64 (tested)
* Windows x86 (untested)
* MacOS x86_64 (tested)
+* MacOS AARCH64/Apple Silicon (untested)
* Linux x86_64 (tested for Ubuntu 20.04)
* Linux ARMv7 & ARMv8 (partially tested in Raspbian but not officially endorsed)
@@ -71,6 +72,21 @@ For bug reporting or feature requesting, use the [issues tab](https://github.com
### Formerly, EOCV-Sim was hosted on a [personal account repo](https://github.com/serivesmejia/EOCV-Sim/). Released prior to 3.0.0 can be found there for historic purposes.
+### [v3.5.0 - New VisionPortal and VisionProcessor API](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.5.0)
+ - This is the 18th release for EOCV-Sim
+ - Changelog
+ - Addresses the changes made in the FTC SDK 8.2 to prepare for the 2023-2024 season:
+ - EOCV-Sim's Viewport implementation has been changed to one using Skiko (Skia) rendering - to address new features implemented in [EasyOpenCV v1.7.0](https://github.com/OpenFTC/EasyOpenCV/releases/tag/v1.7.0)
+ - The VisionPortal & VisionProcessor interfaces have been implemented onto EOCV-Sim - VisionProcessors are treated just like OpenCvPipelines and are automatically detected by the sim to be executed from the user interface.
+ - In order to use the VisionPortal API, OpModes have been added onto the simulator - a new "OpMode" tab on the user interface has been added to address this addition. NOTE: OpModes are only limited to use VisionPortal APIs, other FTC SDK apis such as hardware DcMotor have not been implemented.
+ - A new public API for android.graphics has been adding onto the simulator, translating android.graphics API called by the user into Skiko calls, adding compatibility to the new features in [EasyOpenCV v1.7.0](https://github.com/OpenFTC/EasyOpenCV/releases/tag/v1.7.0) related to canvas drawing.
+ - AprilTagProcessor has also been implemented straight from the SDK, allowing its full API to be used and attached to a VisionProcessor - [see this example OpMode](https://github.com/deltacv/EOCV-Sim/blob/dev/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java).
+ - AprilTagDesktop plugin has been updated to match [EOCV-AprilTag-Plugin v2.0.0](https://github.com/OpenFTC/EOCV-AprilTag-Plugin/releases/tag/v2.0.0)
+ - Support for Apple Silicon Macs has been added to AprilTagDesktop
+ - Several quality of life upgrades to the UI
+ - Bug fixes:
+ - Fixes issues related to pipeline and input source selection - UI components now exclusively react to user interactions as opposed to past versions where changes triggered by EOCV-Sim were picked up as user-made and caused several issues
+
### [v3.4.3 - M1 Mac OpenCV Support](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.4.3)
- This is the 17th release for EOCV-Sim
- Changelog
diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle
index 8ef34278..805fd90b 100644
--- a/TeamCode/build.gradle
+++ b/TeamCode/build.gradle
@@ -1,7 +1,6 @@
import java.nio.file.Paths
plugins {
- id 'java'
id 'org.jetbrains.kotlin.jvm'
}
@@ -9,11 +8,10 @@ apply from: '../build.common.gradle'
dependencies {
implementation project(':EOCV-Sim')
- implementation project(':Common')
- implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version"
+ implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version"
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib"
}
task(runSim, dependsOn: 'classes', type: JavaExec) {
diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java
new file mode 100644
index 00000000..62f8e127
--- /dev/null
+++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTag.java
@@ -0,0 +1,186 @@
+/* Copyright (c) 2023 FIRST. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.robotcontroller.external.samples;
+
+import com.qualcomm.robotcore.eventloop.opmode.Disabled;
+import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
+import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
+import java.util.List;
+import android.util.Size;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;
+import org.firstinspires.ftc.vision.VisionPortal;
+import org.firstinspires.ftc.vision.apriltag.AprilTagDetection;
+import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor;
+import org.firstinspires.ftc.vision.apriltag.AprilTagGameDatabase;
+
+/**
+ * This 2023-2024 OpMode illustrates the basics of AprilTag recognition and pose estimation,
+ * including Java Builder structures for specifying Vision parameters.
+ *
+ * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name.
+ * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list.
+ */
+@TeleOp(name = "Concept: AprilTag", group = "Concept")
+@Disabled
+public class ConceptAprilTag extends LinearOpMode {
+
+ private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera
+
+ /**
+ * {@link #aprilTag} is the variable to store our instance of the AprilTag processor.
+ */
+ private AprilTagProcessor aprilTag;
+
+ /**
+ * {@link #visionPortal} is the variable to store our instance of the vision portal.
+ */
+ private VisionPortal visionPortal;
+
+ @Override
+ public void runOpMode() {
+
+ initAprilTag();
+
+ // Wait for the DS start button to be touched.
+ telemetry.addData("DS preview on/off", "3 dots, Camera Stream");
+ telemetry.addData(">", "Touch Play to start OpMode");
+ telemetry.update();
+
+ waitForStart();
+
+ if (opModeIsActive()) {
+ while (opModeIsActive()) {
+
+ telemetryAprilTag();
+
+ // Push telemetry to the Driver Station.
+ telemetry.update();
+
+ // Share the CPU.
+ sleep(20);
+ }
+ }
+
+ // Save more CPU resources when camera is no longer needed.
+ visionPortal.close();
+
+ } // end method runOpMode()
+
+ /**
+ * Initialize the AprilTag processor.
+ */
+ private void initAprilTag() {
+
+ // Create the AprilTag processor.
+ aprilTag = new AprilTagProcessor.Builder()
+ //.setDrawAxes(false)
+ //.setDrawCubeProjection(false)
+ //.setDrawTagOutline(true)
+ //.setTagFamily(AprilTagProcessor.TagFamily.TAG_36h11)
+ //.setTagLibrary(AprilTagGameDatabase.getCenterStageTagLibrary())
+ //.setOutputUnits(DistanceUnit.INCH, AngleUnit.DEGREES)
+
+ // == CAMERA CALIBRATION ==
+ // If you do not manually specify calibration parameters, the SDK will attempt
+ // to load a predefined calibration for your camera.
+ //.setLensIntrinsics(578.272, 578.272, 402.145, 221.506)
+
+ // ... these parameters are fx, fy, cx, cy.
+
+ .build();
+
+ // Create the vision portal by using a builder.
+ VisionPortal.Builder builder = new VisionPortal.Builder();
+
+ // Set the camera (webcam vs. built-in RC phone camera).
+ if (USE_WEBCAM) {
+ builder.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"));
+ } else {
+ builder.setCamera(BuiltinCameraDirection.BACK);
+ }
+
+ // Choose a camera resolution. Not all cameras support all resolutions.
+ //builder.setCameraResolution(new Size(640, 480));
+
+ // Enable the RC preview (LiveView). Set "false" to omit camera monitoring.
+ //builder.enableCameraMonitoring(true);
+
+ // Set the stream format; MJPEG uses less bandwidth than default YUY2.
+ //builder.setStreamFormat(VisionPortal.StreamFormat.YUY2);
+
+ // Choose whether or not LiveView stops if no processors are enabled.
+ // If set "true", monitor shows solid orange screen if no processors enabled.
+ // If set "false", monitor shows camera view without annotations.
+ //builder.setAutoStopLiveView(false);
+
+ // Set and enable the processor.
+ builder.addProcessor(aprilTag);
+
+ // Build the Vision Portal, using the above settings.
+ visionPortal = builder.build();
+
+ // Disable or re-enable the aprilTag processor at any time.
+ //visionPortal.setProcessorEnabled(aprilTag, true);
+
+ } // end method initAprilTag()
+
+
+ /**
+ * Function to add telemetry about AprilTag detections.
+ */
+ private void telemetryAprilTag() {
+
+ List currentDetections = aprilTag.getDetections();
+ telemetry.addData("# AprilTags Detected", currentDetections.size());
+
+ // Step through the list of detections and display info for each one.
+ for (AprilTagDetection detection : currentDetections) {
+ if (detection.metadata != null) {
+ telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name));
+ telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z));
+ telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw));
+ telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation));
+ } else {
+ telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id));
+ telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y));
+ }
+ } // end for() loop
+
+ // Add "key" information to telemetry
+ telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist.");
+ telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)");
+ telemetry.addLine("RBE = Range, Bearing & Elevation");
+
+ } // end method telemetryAprilTag()
+
+} // end class
\ No newline at end of file
diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java
new file mode 100644
index 00000000..0535fb9b
--- /dev/null
+++ b/TeamCode/src/main/java/org/firstinspires/ftc/robotcontroller/external/samples/ConceptAprilTagEasy.java
@@ -0,0 +1,142 @@
+/* Copyright (c) 2023 FIRST. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.robotcontroller.external.samples;
+
+import com.qualcomm.robotcore.eventloop.opmode.Disabled;
+import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
+import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
+import java.util.List;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+import org.firstinspires.ftc.vision.VisionPortal;
+import org.firstinspires.ftc.vision.apriltag.AprilTagDetection;
+import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor;
+
+/**
+ * This 2023-2024 OpMode illustrates the basics of AprilTag recognition and pose estimation, using
+ * the easy way.
+ *
+ * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name.
+ * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list.
+ */
+@TeleOp(name = "Concept: AprilTag Easy", group = "Concept")
+@Disabled
+public class ConceptAprilTagEasy extends LinearOpMode {
+
+ private static final boolean USE_WEBCAM = true; // true for webcam, false for phone camera
+
+ /**
+ * {@link #aprilTag} is the variable to store our instance of the AprilTag processor.
+ */
+ private AprilTagProcessor aprilTag;
+
+ /**
+ * {@link #visionPortal} is the variable to store our instance of the vision portal.
+ */
+ private VisionPortal visionPortal;
+
+ @Override
+ public void runOpMode() {
+
+ initAprilTag();
+
+ // Wait for the DS start button to be touched.
+ telemetry.addData("DS preview on/off", "3 dots, Camera Stream");
+ telemetry.addData(">", "Touch Play to start OpMode");
+ telemetry.update();
+
+ waitForStart();
+
+ if (opModeIsActive()) {
+ while (opModeIsActive()) {
+
+ telemetryAprilTag();
+
+ // Push telemetry to the Driver Station.
+ telemetry.update();
+
+ // Share the CPU.
+ sleep(20);
+ }
+ }
+
+ // Save more CPU resources when camera is no longer needed.
+ visionPortal.close();
+
+ } // end method runOpMode()
+
+ /**
+ * Initialize the AprilTag processor.
+ */
+ private void initAprilTag() {
+
+ // Create the AprilTag processor the easy way.
+ aprilTag = AprilTagProcessor.easyCreateWithDefaults();
+
+ // Create the vision portal the easy way.
+ if (USE_WEBCAM) {
+ visionPortal = VisionPortal.easyCreateWithDefaults(
+ hardwareMap.get(WebcamName.class, "Webcam 1"), aprilTag);
+ } else {
+ visionPortal = VisionPortal.easyCreateWithDefaults(
+ BuiltinCameraDirection.BACK, aprilTag);
+ }
+
+ } // end method initAprilTag()
+
+ /**
+ * Function to add telemetry about AprilTag detections.
+ */
+ private void telemetryAprilTag() {
+
+ List currentDetections = aprilTag.getDetections();
+ telemetry.addData("# AprilTags Detected", currentDetections.size());
+
+ // Step through the list of detections and display info for each one.
+ for (AprilTagDetection detection : currentDetections) {
+ if (detection.metadata != null) {
+ telemetry.addLine(String.format("\n==== (ID %d) %s", detection.id, detection.metadata.name));
+ telemetry.addLine(String.format("XYZ %6.1f %6.1f %6.1f (inch)", detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z));
+ telemetry.addLine(String.format("PRY %6.1f %6.1f %6.1f (deg)", detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw));
+ telemetry.addLine(String.format("RBE %6.1f %6.1f %6.1f (inch, deg, deg)", detection.ftcPose.range, detection.ftcPose.bearing, detection.ftcPose.elevation));
+ } else {
+ telemetry.addLine(String.format("\n==== (ID %d) Unknown", detection.id));
+ telemetry.addLine(String.format("Center %6.0f %6.0f (pixels)", detection.center.x, detection.center.y));
+ }
+ } // end for() loop
+
+ // Add "key" information to telemetry
+ telemetry.addLine("\nkey:\nXYZ = X (Right), Y (Forward), Z (Up) dist.");
+ telemetry.addLine("PRY = Pitch, Roll & Yaw (XYZ Rotation)");
+ telemetry.addLine("RBE = Range, Bearing & Elevation");
+
+ } // end method telemetryAprilTag()
+
+} // end class
\ No newline at end of file
diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java
index fe16f173..f16770d3 100644
--- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java
+++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java
@@ -23,6 +23,7 @@
import com.qualcomm.robotcore.eventloop.opmode.Disabled;
import org.firstinspires.ftc.robotcore.external.Telemetry;
+import org.firstinspires.ftc.robotcore.external.navigation.*;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
@@ -133,13 +134,16 @@ public Mat processFrame(Mat input)
drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix);
draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix);
+ Orientation rot = Orientation.getOrientation(detection.pose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, AngleUnit.DEGREES);
+
telemetry.addLine(String.format("\nDetected tag ID=%d", detection.id));
telemetry.addLine(String.format("Translation X: %.2f feet", detection.pose.x*FEET_PER_METER));
telemetry.addLine(String.format("Translation Y: %.2f feet", detection.pose.y*FEET_PER_METER));
telemetry.addLine(String.format("Translation Z: %.2f feet", detection.pose.z*FEET_PER_METER));
- telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", Math.toDegrees(detection.pose.yaw)));
- telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", Math.toDegrees(detection.pose.pitch)));
- telemetry.addLine(String.format("Rotation Roll: %.2f degrees", Math.toDegrees(detection.pose.roll)));
+
+ telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", rot.firstAngle));
+ telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", rot.secondAngle));
+ telemetry.addLine(String.format("Rotation Roll: %.2f degrees", rot.thirdAngle));
}
telemetry.update();
diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java
index fea2744e..c6e5fe20 100644
--- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java
+++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java
@@ -170,4 +170,4 @@ public Mat processFrame(Mat input) {
return maskedInputMat;
}
-}
+}
\ No newline at end of file
diff --git a/Vision/build.gradle b/Vision/build.gradle
new file mode 100644
index 00000000..18a23198
--- /dev/null
+++ b/Vision/build.gradle
@@ -0,0 +1,45 @@
+plugins {
+ id 'kotlin'
+ id 'maven-publish'
+}
+
+apply from: '../build.common.gradle'
+
+task sourcesJar(type: Jar) {
+ from sourceSets.main.allJava
+ archiveClassifier = "sources"
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ artifact sourcesJar
+ }
+ }
+}
+
+configurations.all {
+ resolutionStrategy {
+ cacheChangingModulesFor 0, 'seconds'
+ }
+}
+
+dependencies {
+ implementation project(':Common')
+
+ implementation "com.github.deltacv.AprilTagDesktop:AprilTagDesktop:$apriltag_plugin_version"
+
+ api "org.openpnp:opencv:$opencv_version"
+
+ implementation "org.slf4j:slf4j-api:$slf4j_version"
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib'
+
+ // Compatibility: Skiko supports many platforms but we will only be adding
+ // those that are supported by AprilTagDesktop as well
+
+ implementation("org.jetbrains.skiko:skiko-awt-runtime-windows-x64:$skiko_version")
+ implementation("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:$skiko_version")
+ implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-x64:$skiko_version")
+ implementation("org.jetbrains.skiko:skiko-awt-runtime-macos-arm64:$skiko_version")
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/android/graphics/Bitmap.java b/Vision/src/main/java/android/graphics/Bitmap.java
new file mode 100644
index 00000000..fcb2d005
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Bitmap.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package android.graphics;
+
+import org.jetbrains.skia.ColorAlphaType;
+import org.jetbrains.skia.ColorType;
+import org.jetbrains.skia.ImageInfo;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public class Bitmap implements AutoCloseable {
+
+ @Override
+ public void close() throws IOException {
+ theBitmap.close();
+ }
+
+ /**
+ * Possible bitmap configurations. A bitmap configuration describes
+ * how pixels are stored. This affects the quality (color depth) as
+ * well as the ability to display transparent/translucent colors.
+ */
+ public enum Config {
+ // these native values must match up with the enum in SkBitmap.h
+ /**
+ * Each pixel is stored as a single translucency (alpha) channel.
+ * This is very useful to efficiently store masks for instance.
+ * No color information is stored.
+ * With this configuration, each pixel requires 1 byte of memory.
+ */
+ ALPHA_8(1),
+ /**
+ * Each pixel is stored on 2 bytes and only the RGB channels are
+ * encoded: red is stored with 5 bits of precision (32 possible
+ * values), green is stored with 6 bits of precision (64 possible
+ * values) and blue is stored with 5 bits of precision.
+ *
+ * This configuration can produce slight visual artifacts depending
+ * on the configuration of the source. For instance, without
+ * dithering, the result might show a greenish tint. To get better
+ * results dithering should be applied.
+ *
+ * This configuration may be useful when using opaque bitmaps
+ * that do not require high color fidelity.
+ *
+ * Use this formula to pack into 16 bits:
+ *
+ * short color = (R & 0x1f) << 11 | (G & 0x3f) << 5 | (B & 0x1f);
+ *
+ */
+ RGB_565(3),
+ /**
+ * Each pixel is stored on 2 bytes. The three RGB color channels
+ * and the alpha channel (translucency) are stored with a 4 bits
+ * precision (16 possible values.)
+ *
+ * This configuration is mostly useful if the application needs
+ * to store translucency information but also needs to save
+ * memory.
+ *
+ * It is recommended to use {@link #ARGB_8888} instead of this
+ * configuration.
+ *
+ * Note: as of {link android.os.Build.VERSION_CODES#KITKAT},
+ * any bitmap created with this configuration will be created
+ * using {@link #ARGB_8888} instead.
+ *
+ * @deprecated Because of the poor quality of this configuration,
+ * it is advised to use {@link #ARGB_8888} instead.
+ */
+ @Deprecated
+ ARGB_4444(4),
+ /**
+ * Each pixel is stored on 4 bytes. Each channel (RGB and alpha
+ * for translucency) is stored with 8 bits of precision (256
+ * possible values.)
+ *
+ * This configuration is very flexible and offers the best
+ * quality. It should be used whenever possible.
+ *
+ * Use this formula to pack into 32 bits:
+ *
+ * int color = (A & 0xff) << 24 | (B & 0xff) << 16 | (G & 0xff) << 8 | (R & 0xff);
+ *
+ */
+ ARGB_8888(5),
+ /**
+ * Each pixel is stored on 8 bytes. Each channel (RGB and alpha
+ * for translucency) is stored as a
+ * {@link android.util.Half half-precision floating point value}.
+ *
+ * This configuration is particularly suited for wide-gamut and
+ * HDR content.
+ *
+ * Use this formula to pack into 64 bits:
+ *
+ * long color = (A & 0xffff) << 48 | (B & 0xffff) << 32 | (G & 0xffff) << 16 | (R & 0xffff);
+ *
+ */
+ RGBA_F16(6),
+ /**
+ * Special configuration, when bitmap is stored only in graphic memory.
+ * Bitmaps in this configuration are always immutable.
+ *
+ * It is optimal for cases, when the only operation with the bitmap is to draw it on a
+ * screen.
+ */
+ HARDWARE(7),
+ /**
+ * Each pixel is stored on 4 bytes. Each RGB channel is stored with 10 bits of precision
+ * (1024 possible values). There is an additional alpha channel that is stored with 2 bits
+ * of precision (4 possible values).
+ *
+ * This configuration is suited for wide-gamut and HDR content which does not require alpha
+ * blending, such that the memory cost is the same as ARGB_8888 while enabling higher color
+ * precision.
+ *
+ * Use this formula to pack into 32 bits:
+ *
+ * int color = (A & 0x3) << 30 | (B & 0x3ff) << 20 | (G & 0x3ff) << 10 | (R & 0x3ff);
+ *
+ */
+ RGBA_1010102(8);
+
+ final int nativeInt;
+ private static Config sConfigs[] = {
+ null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE, RGBA_1010102
+ };
+ Config(int ni) {
+ this.nativeInt = ni;
+ }
+
+ static Config nativeToConfig(int ni) {
+ return sConfigs[ni];
+ }
+ }
+
+ private static ColorType configToColorType(Config config) {
+ switch (config) {
+ case ALPHA_8:
+ return ColorType.ALPHA_8;
+ case RGB_565:
+ return ColorType.RGB_565;
+ case ARGB_4444:
+ return ColorType.ARGB_4444;
+ case ARGB_8888:
+ return ColorType.BGRA_8888;
+ case RGBA_F16:
+ return ColorType.RGBA_F16;
+ case RGBA_1010102:
+ return ColorType.RGBA_1010102;
+ default:
+ throw new IllegalArgumentException("Unknown config: " + config);
+ }
+ }
+
+ private Config colorTypeToConfig(ColorType colorType) {
+ switch (colorType) {
+ case ALPHA_8:
+ return Config.ALPHA_8;
+ case RGB_565:
+ return Config.RGB_565;
+ case ARGB_4444:
+ return Config.ARGB_4444;
+ case BGRA_8888:
+ return Config.ARGB_8888;
+ case RGBA_F16:
+ return Config.RGBA_F16;
+ case RGBA_1010102:
+ return Config.RGBA_1010102;
+ default:
+ throw new IllegalArgumentException("Unknown colorType: " + colorType);
+ }
+ }
+
+ public static Bitmap createBitmap(int width, int height) {
+ Bitmap bm = new Bitmap();
+ bm.theBitmap.allocPixels(ImageInfo.Companion.makeS32(width, height, ColorAlphaType.PREMUL));
+
+ return bm;
+ }
+
+ public static Bitmap createBitmap(int width, int height, Config config) {
+ Bitmap bm = new Bitmap();
+ bm.theBitmap.allocPixels(new ImageInfo(width, height, configToColorType(config), ColorAlphaType.PREMUL));
+
+ bm.theBitmap.erase(0);
+
+ return bm;
+ }
+
+ public final org.jetbrains.skia.Bitmap theBitmap;
+
+ public Bitmap() {
+ theBitmap = new org.jetbrains.skia.Bitmap();
+ }
+
+ public Bitmap(org.jetbrains.skia.Bitmap bm) {
+ theBitmap = bm;
+ }
+
+ public int getWidth() {
+ return theBitmap.getWidth();
+ }
+
+ public int getHeight() {
+ return theBitmap.getHeight();
+ }
+
+ public Rect getBounds() {
+ return new Rect(0, 0, getWidth(), getHeight());
+ }
+
+ public Config getConfig() {
+ return colorTypeToConfig(theBitmap.getColorType());
+ }
+
+ public void recycle() {
+ theBitmap.close();
+ }
+}
diff --git a/Vision/src/main/java/android/graphics/Canvas.java b/Vision/src/main/java/android/graphics/Canvas.java
new file mode 100644
index 00000000..2e3d8545
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Canvas.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package android.graphics;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.skia.*;
+
+public class Canvas {
+
+ public final org.jetbrains.skia.Canvas theCanvas;
+
+ private Bitmap backingBitmap = null;
+
+ final Object surfaceLock = new Object();
+ private int providedWidth;
+ private int providedHeight;
+
+ public Canvas(Bitmap bitmap) {
+ theCanvas = new org.jetbrains.skia.Canvas(bitmap.theBitmap, new SurfaceProps());
+ backingBitmap = bitmap;
+ }
+
+ public Canvas(Surface surface) {
+ theCanvas = surface.getCanvas();
+
+ providedWidth = surface.getWidth();
+ providedHeight = surface.getHeight();
+ }
+
+ public Canvas(org.jetbrains.skia.Canvas skiaCanvas, int width, int height) {
+ theCanvas = skiaCanvas;
+
+ providedWidth = width;
+ providedHeight = height;
+ }
+
+ public Canvas drawLine(float x, float y, float x1, float y1, Paint paint) {
+ theCanvas.drawLine(x, y, x1, y1, paint.thePaint);
+ return this;
+ }
+
+
+ public void drawRoundRect(float l, float t, float r, float b, float xRad, float yRad, Paint rectPaint) {
+ theCanvas.drawRRect(RRect.makeLTRB(l, t, r, b, xRad, yRad), rectPaint.thePaint);
+ }
+
+ public void drawPath(Path path, Paint paint) {
+ theCanvas.drawPath(path.thePath, paint.thePaint);
+ }
+
+ public void drawCircle(float x, float y, float radius, Paint paint) {
+ theCanvas.drawCircle(x, y, radius, paint.thePaint);
+ }
+
+ public void drawOval(float left, float top, float right, float bottom, Paint paint) {
+ theCanvas.drawOval(new org.jetbrains.skia.Rect(left, top, right, bottom), paint.thePaint);
+ }
+
+ public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) {
+ theCanvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint.thePaint);
+ }
+
+ public void drawText(String text, int start, int end, float x, float y, Paint paint) {
+ Font font = FontCache.makeFont(paint.getTypeface(), paint.getTextSize());
+ theCanvas.drawString(text.substring(start, end), x, y, font, paint.thePaint);
+ }
+
+ public void drawText(String text, float x, float y, Paint paint) {
+ Font font = FontCache.makeFont(paint.getTypeface(), paint.getTextSize());
+ theCanvas.drawString(text, x, y, font, paint.thePaint);
+ }
+
+ public void drawPoints(float[] points, int offset, int count, Paint paint) {
+ // not supported by the skija canvas so we have to do it manually
+ for(int i = offset; i < offset + count; i += 2) {
+ theCanvas.drawPoint(points[i], points[i + 1], paint.thePaint);
+ }
+ }
+
+ public void drawPoints(float[] points, Paint paint) {
+ theCanvas.drawPoints(points, paint.thePaint);
+ }
+
+ public void drawRGB(int r, int g, int b) {
+ theCanvas.clear(Color.rgb(r, g, b));
+ }
+
+ public void drawLines(float[] points, Paint paint) {
+ theCanvas.drawLines(points, paint.thePaint);
+ }
+
+ public void drawRect(Rect rect, Paint paint) {
+ theCanvas.drawRect(rect.toSkijaRect(), paint.thePaint);
+ }
+
+ public void drawRect(float left, float top, float right, float bottom, Paint paint) {
+ theCanvas.drawRect(new org.jetbrains.skia.Rect(left, top, right, bottom), paint.thePaint);
+ }
+
+ public void rotate(float degrees, float xCenter, float yCenter) {
+ theCanvas.rotate(degrees, xCenter, yCenter);
+ }
+
+ public void rotate(float degrees) {
+ theCanvas.rotate(degrees);
+ }
+
+ public int save() {
+ return theCanvas.save();
+ }
+
+ public void restore() {
+ theCanvas.restore();
+ }
+
+ public void drawBitmap(Bitmap bitmap, Rect src, Rect rect, Paint paint) {
+ int left, top, right, bottom;
+ if (src == null) {
+ left = top = 0;
+ right = bitmap.getWidth();
+ bottom = bitmap.getHeight();
+ } else {
+ left = src.left;
+ top = src.top;
+ right = src.right;
+ bottom = src.bottom;
+ }
+
+ org.jetbrains.skia.Paint thePaint = null;
+
+ if(paint != null) {
+ thePaint = paint.thePaint;
+ }
+
+ theCanvas.drawImageRect(
+ Image.Companion.makeFromBitmap(bitmap.theBitmap),
+ new org.jetbrains.skia.Rect(left, top, right, bottom),
+ rect.toSkijaRect(), thePaint
+ );
+ }
+
+ public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
+ org.jetbrains.skia.Paint thePaint = null;
+
+ if(paint != null) {
+ thePaint = paint.thePaint;
+ }
+
+ theCanvas.drawImage(Image.Companion.makeFromBitmap(bitmap.theBitmap), left, top, thePaint);
+ }
+
+ public void skew(float sx, float sy) {
+ theCanvas.skew(sx, sy);
+ }
+
+ public void translate(int dx, int dy) {
+ theCanvas.translate(dx, dy);
+ }
+
+ public void scale(float sx, float sy) {
+ theCanvas.scale(sx, sy);
+ }
+
+ public void restoreToCount(int saveCount) {
+ theCanvas.restoreToCount(saveCount);
+ }
+
+ public boolean readPixels(@NotNull Bitmap lastFrame, int srcX, int srcY) {
+ return theCanvas.readPixels(lastFrame.theBitmap, srcX, srcY);
+ }
+
+ public Canvas drawColor(int color) {
+ theCanvas.clear(color);
+ return this;
+ }
+
+ public int getWidth() {
+ if(backingBitmap != null) {
+ return backingBitmap.getWidth();
+ }
+
+ return providedWidth;
+ }
+
+ public int getHeight() {
+ if(backingBitmap != null) {
+ return backingBitmap.getHeight();
+ }
+
+ return providedHeight;
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/android/graphics/Color.java b/Vision/src/main/java/android/graphics/Color.java
new file mode 100644
index 00000000..7460ec3e
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Color.java
@@ -0,0 +1,1415 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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 android.graphics;
+import android.annotation.AnyThread;
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.HalfFloat;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SuppressAutoDoc;
+import android.util.Half;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.function.DoubleUnaryOperator;
+/**
+ * {@usesMathJax}
+ *
+ * The Color
class provides methods for creating, converting and
+ * manipulating colors. Colors have three different representations:
+ *
+ * - Color ints, the most common representation
+ * - Color longs
+ * Color
instances
+ *
+ * The section below describe each representation in detail.
+ *
+ * Color ints
+ * Color ints are the most common representation of colors on Android and
+ * have been used since {link android.os.Build.VERSION_CODES#BASE API level 1}.
+ *
+ * A color int always defines a color in the {link ColorSpace.Named#SRGB sRGB}
+ * color space using 4 components packed in a single 32 bit integer value:
+ *
+ *
+ *
+ * Component | Name | Size | Range |
+ *
+ * A | Alpha | 8 bits | \([0..255]\) |
+ * R | Red | 8 bits | \([0..255]\) |
+ * G | Green | 8 bits | \([0..255]\) |
+ * B | Blue | 8 bits | \([0..255]\) |
+ *
+ *
+ * The components in this table are listed in encoding order (see below),
+ * which is why color ints are called ARGB colors.
+ *
+ * Usage in code
+ * To avoid confusing color ints with arbitrary integer values, it is a
+ * good practice to annotate them with the @ColorInt
annotation
+ * found in the Android Support Library.
+ *
+ * Encoding
+ * The four components of a color int are encoded in the following way:
+ *
+ * int color = (A & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);
+ *
+ *
+ * Because of this encoding, color ints can easily be described as an integer
+ * constant in source. For instance, opaque blue is 0xff0000ff
+ * and yellow is 0xffffff00
.
+ *
+ * To easily encode color ints, it is recommended to use the static methods
+ * {@link #argb(int, int, int, int)} and {@link #rgb(int, int, int)}. The second
+ * method omits the alpha component and assumes the color is opaque (alpha is 255).
+ * As a convenience this class also offers methods to encode color ints from components
+ * defined in the \([0..1]\) range: {@link #argb(float, float, float, float)} and
+ * {@link #rgb(float, float, float)}.
+ *
+ * Color longs (defined below) can be easily converted to color ints by invoking
+ * the {@link #toArgb(long)} method. This method performs a color space conversion
+ * if needed.
+ *
+ * It is also possible to create a color int by invoking the method {@link #toArgb()}
+ * on a color instance.
+ *
+ * Decoding
+ * The four ARGB components can be individually extracted from a color int
+ * using the following expressions:
+ *
+ * int A = (color >> 24) & 0xff; // or color >>> 24
+ * int R = (color >> 16) & 0xff;
+ * int G = (color >> 8) & 0xff;
+ * int B = (color ) & 0xff;
+ *
+ *
+ * This class offers convenience methods to easily extract these components:
+ *
+ * - {@link #alpha(int)} to extract the alpha component
+ * - {@link #red(int)} to extract the red component
+ * - {@link #green(int)} to extract the green component
+ * - {@link #blue(int)} to extract the blue component
+ *
+ *
+ * Color longs
+ * Color longs are a representation introduced in
+ * {link android.os.Build.VERSION_CODES#O Android O} to store colors in different
+ * {@link ColorSpace color spaces}, with more precision than color ints.
+ *
+ * A color long always defines a color using 4 components packed in a single
+ * 64 bit long value. One of these components is always alpha while the other
+ * three components depend on the color space's {@link ColorSpace.Model color model}.
+ * The most common color model is the {@link ColorSpace.Model#RGB RGB} model in
+ * which the components represent red, green and blue values.
+ *
+ * Component ranges: the ranges defined in the tables
+ * below indicate the ranges that can be encoded in a color long. They do not
+ * represent the actual ranges as they may differ per color space. For instance,
+ * the RGB components of a color in the {@link ColorSpace.Named#DISPLAY_P3 Display P3}
+ * color space use the \([0..1]\) range. Please refer to the documentation of the
+ * various {@link ColorSpace.Named color spaces} to find their respective ranges.
+ *
+ * Alpha range: while alpha is encoded in a color long using
+ * a 10 bit integer (thus using a range of \([0..1023]\)), it is converted to and
+ * from \([0..1]\) float values when decoding and encoding color longs.
+ *
+ * sRGB color space: for compatibility reasons and ease of
+ * use, color longs encoding {@link ColorSpace.Named#SRGB sRGB} colors do not
+ * use the same encoding as other color longs.
+ *
+ *
+ *
+ * Component | Name | Size | Range |
+ *
+ * {@link ColorSpace.Model#RGB RGB} color model |
+ * R | Red | 16 bits | \([-65504.0, 65504.0]\) |
+ * G | Green | 16 bits | \([-65504.0, 65504.0]\) |
+ * B | Blue | 16 bits | \([-65504.0, 65504.0]\) |
+ * A | Alpha | 10 bits | \([0..1023]\) |
+ * | Color space | 6 bits | \([0..63]\) |
+ * {@link ColorSpace.Named#SRGB sRGB} color space |
+ * A | Alpha | 8 bits | \([0..255]\) |
+ * R | Red | 8 bits | \([0..255]\) |
+ * G | Green | 8 bits | \([0..255]\) |
+ * B | Blue | 8 bits | \([0..255]\) |
+ * X | Unused | 32 bits | \(0\) |
+ * {@link ColorSpace.Model#XYZ XYZ} color model |
+ * X | X | 16 bits | \([-65504.0, 65504.0]\) |
+ * Y | Y | 16 bits | \([-65504.0, 65504.0]\) |
+ * Z | Z | 16 bits | \([-65504.0, 65504.0]\) |
+ * A | Alpha | 10 bits | \([0..1023]\) |
+ * | Color space | 6 bits | \([0..63]\) |
+ * {@link ColorSpace.Model#XYZ Lab} color model |
+ * L | L | 16 bits | \([-65504.0, 65504.0]\) |
+ * a | a | 16 bits | \([-65504.0, 65504.0]\) |
+ * b | b | 16 bits | \([-65504.0, 65504.0]\) |
+ * A | Alpha | 10 bits | \([0..1023]\) |
+ * | Color space | 6 bits | \([0..63]\) |
+ * {@link ColorSpace.Model#CMYK CMYK} color model |
+ * Unsupported |
+ *
+ *
+ * The components in this table are listed in encoding order (see below),
+ * which is why color longs in the RGB model are called RGBA colors (even if
+ * this doesn't quite hold for the special case of sRGB colors).
+ *
+ * The color long encoding relies on half-precision float values (fp16). If you
+ * wish to know more about the limitations of half-precision float values, please
+ * refer to the documentation of the {@link Half} class.
+ *
+ * Usage in code
+ * To avoid confusing color longs with arbitrary long values, it is a
+ * good practice to annotate them with the @ColorLong
annotation
+ * found in the Android Support Library.
+ *
+ * Encoding
+ *
+ * Given the complex nature of color longs, it is strongly encouraged to use
+ * the various methods provided by this class to encode them.
+ *
+ * The most flexible way to encode a color long is to use the method
+ * {@link #pack(float, float, float, float, ColorSpace)}. This method allows you
+ * to specify three color components (typically RGB), an alpha component and a
+ * color space. To encode sRGB colors, use {@link #pack(float, float, float)}
+ * and {@link #pack(float, float, float, float)} which are the
+ * equivalent of {@link #rgb(int, int, int)} and {@link #argb(int, int, int, int)}
+ * for color ints. If you simply need to convert a color int into a color long,
+ * use {@link #pack(int)}.
+ *
+ * It is also possible to create a color long value by invoking the method
+ * {@link #pack()} on a color instance.
+ *
+ * Decoding
+ *
+ * This class offers convenience methods to easily extract the components
+ * of a color long:
+ *
+ * - {@link #alpha(long)} to extract the alpha component
+ * - {@link #red(long)} to extract the red/X/L component
+ * - {@link #green(long)} to extract the green/Y/a component
+ * - {@link #blue(long)} to extract the blue/Z/b component
+ *
+ *
+ * The values returned by these methods depend on the color space encoded
+ * in the color long. The values are however typically in the \([0..1]\) range
+ * for RGB colors. Please refer to the documentation of the various
+ * {@link ColorSpace.Named color spaces} for the exact ranges.
+ *
+ * Color instances
+ * Color instances are a representation introduced in
+ * {link android.os.Build.VERSION_CODES#O Android O} to store colors in different
+ * {@link ColorSpace color spaces}, with more precision than both color ints and
+ * color longs. Color instances also offer the ability to store more than 4
+ * components if necessary.
+ *
+ * Colors instances are immutable and can be created using one of the various
+ * valueOf
methods. For instance:
+ *
+ * // sRGB
+ * Color opaqueRed = Color.valueOf(0xffff0000); // from a color int
+ * Color translucentRed = Color.valueOf(1.0f, 0.0f, 0.0f, 0.5f);
+ *
+ * // Wide gamut color
+ * {@literal @}ColorLong long p3 = pack(1.0f, 1.0f, 0.0f, 1.0f, colorSpaceP3);
+ * Color opaqueYellow = Color.valueOf(p3); // from a color long
+ *
+ * // CIE L*a*b* color space
+ * ColorSpace lab = ColorSpace.get(ColorSpace.Named.LAB);
+ * Color green = Color.valueOf(100.0f, -128.0f, 128.0f, 1.0f, lab);
+ *
+ *
+ * Color instances can be converted to color ints ({@link #toArgb()}) or
+ * color longs ({@link #pack()}). They also offer easy access to their various
+ * components using the following methods:
+ *
+ * - {@link #alpha()}, returns the alpha component value
+ * - {@link #red()}, returns the red component value (or first
+ * component value in non-RGB models)
+ * - {@link #green()}, returns the green component value (or second
+ * component value in non-RGB models)
+ * - {@link #blue()}, returns the blue component value (or third
+ * component value in non-RGB models)
+ * - {@link #getComponent(int)}, returns a specific component value
+ * - {@link #getComponents()}, returns all component values as an array
+ *
+ *
+ * Color space conversions
+ * You can convert colors from one color space to another using
+ * {@link ColorSpace#connect(ColorSpace, ColorSpace)} and its variants. However,
+ * the Color
class provides a few convenience methods to simplify
+ * the process. Here is a brief description of some of them:
+ *
+ * - {@link #convert(ColorSpace)} to convert a color instance in a color
+ * space to a new color instance in a different color space
+ * - {@link #convert(float, float, float, float, ColorSpace, ColorSpace)} to
+ * convert a color from a source color space to a destination color space
+ * - {@link #convert(long, ColorSpace)} to convert a color long from its
+ * built-in color space to a destination color space
+ * - {@link #convert(int, ColorSpace)} to convert a color int from sRGB
+ * to a destination color space
+ *
+ *
+ * Please refere to the {@link ColorSpace} documentation for more
+ * information.
+ *
+ * Alpha and transparency
+ * The alpha component of a color defines the level of transparency of a
+ * color. When the alpha component is 0, the color is completely transparent.
+ * When the alpha is component is 1 (in the \([0..1]\) range) or 255 (in the
+ * \([0..255]\) range), the color is completely opaque.
+ *
+ * The color representations described above do not use pre-multiplied
+ * color components (a pre-multiplied color component is a color component
+ * that has been multiplied by the value of the alpha component).
+ * For instance, the color int representation of opaque red is
+ * 0xffff0000
. For semi-transparent (50%) red, the
+ * representation becomes 0x80ff0000
. The equivalent color
+ * instance representations would be (1.0, 0.0, 0.0, 1.0)
+ * and (1.0, 0.0, 0.0, 0.5)
.
+ */
+@AnyThread
+@SuppressAutoDoc
+public class Color {
+ @ColorInt public static final int BLACK = 0xFF000000;
+ @ColorInt public static final int DKGRAY = 0xFF444444;
+ @ColorInt public static final int GRAY = 0xFF888888;
+ @ColorInt public static final int LTGRAY = 0xFFCCCCCC;
+ @ColorInt public static final int WHITE = 0xFFFFFFFF;
+ @ColorInt public static final int RED = 0xFFFF0000;
+ @ColorInt public static final int GREEN = 0xFF00FF00;
+ @ColorInt public static final int BLUE = 0xFF0000FF;
+ @ColorInt public static final int YELLOW = 0xFFFFFF00;
+ @ColorInt public static final int CYAN = 0xFF00FFFF;
+ @ColorInt public static final int MAGENTA = 0xFFFF00FF;
+ @ColorInt public static final int TRANSPARENT = 0;
+ @NonNull
+ @Size(min = 4, max = 5)
+ private final float[] mComponents;
+ @NonNull
+ private final ColorSpace mColorSpace;
+ /**
+ * Creates a new color instance set to opaque black in the
+ * {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @see #valueOf(float, float, float)
+ * @see #valueOf(float, float, float, float)
+ * @see #valueOf(float, float, float, float, ColorSpace)
+ * @see #valueOf(float[], ColorSpace)
+ * @see #valueOf(int)
+ * @see #valueOf(long)
+ */
+ public Color() {
+ // This constructor is required for compatibility with previous APIs
+ mComponents = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
+ mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ }
+ /**
+ * Creates a new color instance in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param r The value of the red channel, must be in [0..1] range
+ * @param g The value of the green channel, must be in [0..1] range
+ * @param b The value of the blue channel, must be in [0..1] range
+ * @param a The value of the alpha channel, must be in [0..1] range
+ */
+ private Color(float r, float g, float b, float a) {
+ this(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+ /**
+ * Creates a new color instance in the specified color space. The color space
+ * must have a 3 components model.
+ *
+ * @param r The value of the red channel, must be in the color space defined range
+ * @param g The value of the green channel, must be in the color space defined range
+ * @param b The value of the blue channel, must be in the color space defined range
+ * @param a The value of the alpha channel, must be in [0..1] range
+ * @param colorSpace This color's color space, cannot be null
+ */
+ private Color(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
+ mComponents = new float[] { r, g, b, a };
+ mColorSpace = colorSpace;
+ }
+ /**
+ * Creates a new color instance in the specified color space.
+ *
+ * @param components An array of color components, plus alpha
+ * @param colorSpace This color's color space, cannot be null
+ */
+ private Color(@Size(min = 4, max = 5) float[] components, @NonNull ColorSpace colorSpace) {
+ mComponents = components;
+ mColorSpace = colorSpace;
+ }
+ /**
+ * Returns this color's color space.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ */
+ @NonNull
+ public ColorSpace getColorSpace() {
+ return mColorSpace;
+ }
+ /**
+ * Returns the color model of this color.
+ *
+ * @return A non-null {@link ColorSpace.Model}
+ */
+ public ColorSpace.Model getModel() {
+ return mColorSpace.getModel();
+ }
+ /**
+ * Indicates whether this color color is in a wide-gamut color space.
+ * See {@link ColorSpace#isWideGamut()} for a definition of a wide-gamut
+ * color space.
+ *
+ * @return True if this color is in a wide-gamut color space, false otherwise
+ *
+ * @see #isSrgb()
+ * @see ColorSpace#isWideGamut()
+ */
+ public boolean isWideGamut() {
+ return getColorSpace().isWideGamut();
+ }
+ /**
+ * Indicates whether this color is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @return True if this color is in the sRGB color space, false otherwise
+ *
+ * @see #isWideGamut()
+ */
+ public boolean isSrgb() {
+ return getColorSpace().isSrgb();
+ }
+ /**
+ * Returns the number of components that form a color value according
+ * to this color space's color model, plus one extra component for
+ * alpha.
+ *
+ * @return The integer 4 or 5
+ */
+ @IntRange(from = 4, to = 5)
+ public int getComponentCount() {
+ return mColorSpace.getComponentCount() + 1;
+ }
+ /**
+ * Packs this color into a color long. See the documentation of this class
+ * for a description of the color long format.
+ *
+ * @return A color long
+ *
+ * @throws IllegalArgumentException If this color's color space has the id
+ * {@link ColorSpace#MIN_ID} or if this color has more than 4 components
+ */
+ @ColorLong
+ public long pack() {
+ return pack(mComponents[0], mComponents[1], mComponents[2], mComponents[3], mColorSpace);
+ }
+ /**
+ * Converts this color from its color space to the specified color space.
+ * The conversion is done using the default rendering intent as specified
+ * by {@link ColorSpace#connect(ColorSpace, ColorSpace)}.
+ *
+ * @param colorSpace The destination color space, cannot be null
+ *
+ * @return A non-null color instance in the specified color space
+ */
+ @NonNull
+ public Color convert(@NonNull ColorSpace colorSpace) {
+ ColorSpace.Connector connector = ColorSpace.connect(mColorSpace, colorSpace);
+ float[] color = new float[] {
+ mComponents[0], mComponents[1], mComponents[2], mComponents[3]
+ };
+ connector.transform(color);
+ return new Color(color, colorSpace);
+ }
+ /**
+ * Converts this color to an ARGB color int. A color int is always in
+ * the {@link ColorSpace.Named#SRGB sRGB} color space. This implies
+ * a color space conversion is applied if needed.
+ *
+ * @return An ARGB color in the sRGB color space
+ */
+ @ColorInt
+ public int toArgb() {
+ if (mColorSpace.isSrgb()) {
+ return ((int) (mComponents[3] * 255.0f + 0.5f) << 24) |
+ ((int) (mComponents[0] * 255.0f + 0.5f) << 16) |
+ ((int) (mComponents[1] * 255.0f + 0.5f) << 8) |
+ (int) (mComponents[2] * 255.0f + 0.5f);
+ }
+ float[] color = new float[] {
+ mComponents[0], mComponents[1], mComponents[2], mComponents[3]
+ };
+ // The transformation saturates the output
+ ColorSpace.connect(mColorSpace).transform(color);
+ return ((int) (color[3] * 255.0f + 0.5f) << 24) |
+ ((int) (color[0] * 255.0f + 0.5f) << 16) |
+ ((int) (color[1] * 255.0f + 0.5f) << 8) |
+ (int) (color[2] * 255.0f + 0.5f);
+ }
+ /**
+ * Returns the value of the red component in the range defined by this
+ * color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).
+ *
+ * If this color's color model is not {@link ColorSpace.Model#RGB RGB},
+ * calling this method is equivalent to getComponent(0)
.
+ *
+ * @see #alpha()
+ * @see #red()
+ * @see #green
+ * @see #getComponents()
+ */
+ public float red() {
+ return mComponents[0];
+ }
+ /**
+ * Returns the value of the green component in the range defined by this
+ * color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).
+ *
+ * If this color's color model is not {@link ColorSpace.Model#RGB RGB},
+ * calling this method is equivalent to getComponent(1)
.
+ *
+ * @see #alpha()
+ * @see #red()
+ * @see #green
+ * @see #getComponents()
+ */
+ public float green() {
+ return mComponents[1];
+ }
+ /**
+ * Returns the value of the blue component in the range defined by this
+ * color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).
+ *
+ * If this color's color model is not {@link ColorSpace.Model#RGB RGB},
+ * calling this method is equivalent to getComponent(2)
.
+ *
+ * @see #alpha()
+ * @see #red()
+ * @see #green
+ * @see #getComponents()
+ */
+ public float blue() {
+ return mComponents[2];
+ }
+ /**
+ * Returns the value of the alpha component in the range \([0..1]\).
+ * Calling this method is equivalent to
+ * getComponent(getComponentCount() - 1)
.
+ *
+ * @see #red()
+ * @see #green()
+ * @see #blue()
+ * @see #getComponents()
+ * @see #getComponent(int)
+ */
+ public float alpha() {
+ return mComponents[mComponents.length - 1];
+ }
+ /**
+ * Returns this color's components as a new array. The last element of the
+ * array is always the alpha component.
+ *
+ * @return A new, non-null array whose size is equal to {@link #getComponentCount()}
+ *
+ * @see #getComponent(int)
+ */
+ @NonNull
+ @Size(min = 4, max = 5)
+ public float[] getComponents() {
+ return Arrays.copyOf(mComponents, mComponents.length);
+ }
+ /**
+ * Copies this color's components in the supplied array. The last element of the
+ * array is always the alpha component.
+ *
+ * @param components An array of floats whose size must be at least
+ * {@link #getComponentCount()}, can be null
+ * @return The array passed as a parameter if not null, or a new array of length
+ * {@link #getComponentCount()}
+ *
+ * @see #getComponent(int)
+ *
+ * @throws IllegalArgumentException If the specified array's length is less than
+ * {@link #getComponentCount()}
+ */
+ @NonNull
+ @Size(min = 4)
+ public float[] getComponents(@Nullable @Size(min = 4) float[] components) {
+ if (components == null) {
+ return Arrays.copyOf(mComponents, mComponents.length);
+ }
+ if (components.length < mComponents.length) {
+ throw new IllegalArgumentException("The specified array's length must be at "
+ + "least " + mComponents.length);
+ }
+ System.arraycopy(mComponents, 0, components, 0, mComponents.length);
+ return components;
+ }
+ /**
+ * Returns the value of the specified component in the range defined by
+ * this color's color space (see {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}).
+ *
+ * If the requested component index is {@link #getComponentCount()},
+ * this method returns the alpha component, always in the range
+ * \([0..1]\).
+ *
+ * @see #getComponents()
+ *
+ * @throws ArrayIndexOutOfBoundsException If the specified component index
+ * is < 0 or >= {@link #getComponentCount()}
+ */
+ public float getComponent(@IntRange(from = 0, to = 4) int component) {
+ return mComponents[component];
+ }
+ /**
+ * Returns the relative luminance of this color.
+ *
+ * Based on the formula for relative luminance defined in WCAG 2.0,
+ * W3C Recommendation 11 December 2008.
+ *
+ * @return A value between 0 (darkest black) and 1 (lightest white)
+ *
+ * @throws IllegalArgumentException If the this color's color space
+ * does not use the {@link ColorSpace.Model#RGB RGB} color model
+ */
+ public float luminance() {
+ if (mColorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
+ "color space. The supplied color space is " + mColorSpace.getModel());
+ }
+ DoubleUnaryOperator eotf = ((ColorSpace.Rgb) mColorSpace).getEotf();
+ double r = eotf.applyAsDouble(mComponents[0]);
+ double g = eotf.applyAsDouble(mComponents[1]);
+ double b = eotf.applyAsDouble(mComponents[2]);
+ return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Color color = (Color) o;
+ //noinspection SimplifiableIfStatement
+ if (!Arrays.equals(mComponents, color.mComponents)) return false;
+ return mColorSpace.equals(color.mColorSpace);
+ }
+ @Override
+ public int hashCode() {
+ int result = Arrays.hashCode(mComponents);
+ result = 31 * result + mColorSpace.hashCode();
+ return result;
+ }
+ /**
+ * Returns a string representation of the object. This method returns
+ * a string equal to the value of:
+ *
+ *
+ * "Color(" + r + ", " + g + ", " + b + ", " + a +
+ * ", " + getColorSpace().getName + ')'
+ *
+ *
+ * For instance, the string representation of opaque black in the sRGB
+ * color space is equal to the following value:
+ *
+ *
+ * Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1)
+ *
+ *
+ * @return A non-null string representation of the object
+ */
+ @Override
+ @NonNull
+ public String toString() {
+ StringBuilder b = new StringBuilder("Color(");
+ for (float c : mComponents) {
+ b.append(c).append(", ");
+ }
+ b.append(mColorSpace.getName());
+ b.append(')');
+ return b.toString();
+ }
+ /**
+ * Returns the color space encoded in the specified color long.
+ *
+ * @param color The color long whose color space to extract
+ * @return A non-null color space instance
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ *
+ * @see #red(long)
+ * @see #green(long)
+ * @see #blue(long)
+ * @see #alpha(long)
+ */
+ @NonNull
+ public static ColorSpace colorSpace(@ColorLong long color) {
+ return ColorSpace.get((int) (color & 0x3fL));
+ }
+ /**
+ * Returns the red component encoded in the specified color long.
+ * The range of the returned value depends on the color space
+ * associated with the specified color. The color space can be
+ * queried by calling {@link #colorSpace(long)}.
+ *
+ * @param color The color long whose red channel to extract
+ * @return A float value with a range defined by the specified color's
+ * color space
+ *
+ * @see #colorSpace(long)
+ * @see #green(long)
+ * @see #blue(long)
+ * @see #alpha(long)
+ */
+ public static float red(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 48) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 48) & 0xffff));
+ }
+ /**
+ * Returns the green component encoded in the specified color long.
+ * The range of the returned value depends on the color space
+ * associated with the specified color. The color space can be
+ * queried by calling {@link #colorSpace(long)}.
+ *
+ * @param color The color long whose green channel to extract
+ * @return A float value with a range defined by the specified color's
+ * color space
+ *
+ * @see #colorSpace(long)
+ * @see #red(long)
+ * @see #blue(long)
+ * @see #alpha(long)
+ */
+ public static float green(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 40) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 32) & 0xffff));
+ }
+ /**
+ * Returns the blue component encoded in the specified color long.
+ * The range of the returned value depends on the color space
+ * associated with the specified color. The color space can be
+ * queried by calling {@link #colorSpace(long)}.
+ *
+ * @param color The color long whose blue channel to extract
+ * @return A float value with a range defined by the specified color's
+ * color space
+ *
+ * @see #colorSpace(long)
+ * @see #red(long)
+ * @see #green(long)
+ * @see #alpha(long)
+ */
+ public static float blue(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 32) & 0xff) / 255.0f;
+ return Half.toFloat((short) ((color >> 16) & 0xffff));
+ }
+ /**
+ * Returns the alpha component encoded in the specified color long.
+ * The returned value is always in the range \([0..1]\).
+ *
+ * @param color The color long whose alpha channel to extract
+ * @return A float value in the range \([0..1]\)
+ *
+ * @see #colorSpace(long)
+ * @see #red(long)
+ * @see #green(long)
+ * @see #blue(long)
+ */
+ public static float alpha(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return ((color >> 56) & 0xff) / 255.0f;
+ return ((color >> 6) & 0x3ff) / 1023.0f;
+ }
+ /**
+ * Indicates whether the specified color is in the
+ * {@link ColorSpace.Named#SRGB sRGB} color space.
+ *
+ * @param color The color to test
+ * @return True if the color is in the sRGB color space, false otherwise
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ *
+ * @see #isInColorSpace(long, ColorSpace)
+ * @see #isWideGamut(long)
+ */
+ public static boolean isSrgb(@ColorLong long color) {
+ return colorSpace(color).isSrgb();
+ }
+ /**
+ * Indicates whether the specified color is in a wide-gamut color space.
+ * See {@link ColorSpace#isWideGamut()} for a definition of a wide-gamut
+ * color space.
+ *
+ * @param color The color to test
+ * @return True if the color is in a wide-gamut color space, false otherwise
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ *
+ * @see #isInColorSpace(long, ColorSpace)
+ * @see #isSrgb(long)
+ * @see ColorSpace#isWideGamut()
+ */
+ public static boolean isWideGamut(@ColorLong long color) {
+ return colorSpace(color).isWideGamut();
+ }
+ /**
+ * Indicates whether the specified color is in the specified color space.
+ *
+ * @param color The color to test
+ * @param colorSpace The color space to test against
+ * @return True if the color is in the specified color space, false otherwise
+ *
+ * @see #isSrgb(long)
+ * @see #isWideGamut(long)
+ */
+ public static boolean isInColorSpace(@ColorLong long color, @NonNull ColorSpace colorSpace) {
+ return (int) (color & 0x3fL) == colorSpace.getId();
+ }
+ /**
+ * Converts the specified color long to an ARGB color int. A color int is
+ * always in the {@link ColorSpace.Named#SRGB sRGB} color space. This implies
+ * a color space conversion is applied if needed.
+ *
+ * @return An ARGB color in the sRGB color space
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ */
+ @ColorInt
+ public static int toArgb(@ColorLong long color) {
+ if ((color & 0x3fL) == 0L) return (int) (color >> 32);
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ // The transformation saturates the output
+ float[] c = ColorSpace.connect(colorSpace(color)).transform(r, g, b);
+ return ((int) (a * 255.0f + 0.5f) << 24) |
+ ((int) (c[0] * 255.0f + 0.5f) << 16) |
+ ((int) (c[1] * 255.0f + 0.5f) << 8) |
+ (int) (c[2] * 255.0f + 0.5f);
+ }
+ /**
+ * Creates a new Color
instance from an ARGB color int.
+ * The resulting color is in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space.
+ *
+ * @param color The ARGB color int to create a Color
from
+ * @return A non-null instance of {@link Color}
+ */
+ @NonNull
+ public static Color valueOf(@ColorInt int color) {
+ float r = ((color >> 16) & 0xff) / 255.0f;
+ float g = ((color >> 8) & 0xff) / 255.0f;
+ float b = ((color ) & 0xff) / 255.0f;
+ float a = ((color >> 24) & 0xff) / 255.0f;
+ return new Color(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+ /**
+ * Creates a new Color
instance from a color long.
+ * The resulting color is in the same color space as the specified color long.
+ *
+ * @param color The color long to create a Color
from
+ * @return A non-null instance of {@link Color}
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ */
+ @NonNull
+ public static Color valueOf(@ColorLong long color) {
+ return new Color(red(color), green(color), blue(color), alpha(color), colorSpace(color));
+ }
+ /**
+ * Creates a new opaque Color
in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space with the specified red, green and blue component values. The component
+ * values must be in the range \([0..1]\).
+ *
+ * @param r The red component of the opaque sRGB color to create, in \([0..1]\)
+ * @param g The green component of the opaque sRGB color to create, in \([0..1]\)
+ * @param b The blue component of the opaque sRGB color to create, in \([0..1]\)
+ * @return A non-null instance of {@link Color}
+ */
+ @NonNull
+ public static Color valueOf(float r, float g, float b) {
+ return new Color(r, g, b, 1.0f);
+ }
+ /**
+ * Creates a new Color
in the {@link ColorSpace.Named#SRGB sRGB}
+ * color space with the specified red, green, blue and alpha component values.
+ * The component values must be in the range \([0..1]\).
+ *
+ * @param r The red component of the sRGB color to create, in \([0..1]\)
+ * @param g The green component of the sRGB color to create, in \([0..1]\)
+ * @param b The blue component of the sRGB color to create, in \([0..1]\)
+ * @param a The alpha component of the sRGB color to create, in \([0..1]\)
+ * @return A non-null instance of {@link Color}
+ */
+ @NonNull
+ public static Color valueOf(float r, float g, float b, float a) {
+ return new Color(saturate(r), saturate(g), saturate(b), saturate(a));
+ }
+ /**
+ * Creates a new Color
in the specified color space with the
+ * specified red, green, blue and alpha component values. The range of the
+ * components is defined by {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}. The values passed to this method
+ * must be in the proper range.
+ *
+ * @param r The red component of the color to create
+ * @param g The green component of the color to create
+ * @param b The blue component of the color to create
+ * @param a The alpha component of the color to create, in \([0..1]\)
+ * @param colorSpace The color space of the color to create
+ * @return A non-null instance of {@link Color}
+ *
+ * @throws IllegalArgumentException If the specified color space uses a
+ * color model with more than 3 components
+ */
+ @NonNull
+ public static Color valueOf(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
+ if (colorSpace.getComponentCount() > 3) {
+ throw new IllegalArgumentException("The specified color space must use a color model " +
+ "with at most 3 color components");
+ }
+ return new Color(r, g, b, a, colorSpace);
+ }
+ /**
+ * Creates a new Color
in the specified color space with the
+ * specified component values. The range of the components is defined by
+ * {@link ColorSpace#getMinValue(int)} and {@link ColorSpace#getMaxValue(int)}.
+ * The values passed to this method must be in the proper range. The alpha
+ * component is always in the range \([0..1]\).
+ *
+ * The length of the array of components must be at least
+ * {@link ColorSpace#getComponentCount()} + 1
. The component at index
+ * {@link ColorSpace#getComponentCount()} is always alpha.
+ *
+ * @param components The components of the color to create, with alpha as the last component
+ * @param colorSpace The color space of the color to create
+ * @return A non-null instance of {@link Color}
+ *
+ * @throws IllegalArgumentException If the array of components is smaller than
+ * required by the color space
+ */
+ @NonNull
+ public static Color valueOf(@NonNull @Size(min = 4, max = 5) float[] components,
+ @NonNull ColorSpace colorSpace) {
+ if (components.length < colorSpace.getComponentCount() + 1) {
+ throw new IllegalArgumentException("Received a component array of length " +
+ components.length + " but the color model requires " +
+ (colorSpace.getComponentCount() + 1) + " (including alpha)");
+ }
+ return new Color(Arrays.copyOf(components, colorSpace.getComponentCount() + 1), colorSpace);
+ }
+ /**
+ * Converts the specified ARGB color int to an RGBA color long in the sRGB
+ * color space. See the documentation of this class for a description of
+ * the color long format.
+ *
+ * @param color The ARGB color int to convert to an RGBA color long in sRGB
+ *
+ * @return A color long
+ */
+ @ColorLong
+ public static long pack(@ColorInt int color) {
+ return (color & 0xffffffffL) << 32;
+ }
+ /**
+ * Packs the sRGB color defined by the specified red, green and blue component
+ * values into an RGBA color long in the sRGB color space. The alpha component
+ * is set to 1.0. See the documentation of this class for a description of the
+ * color long format.
+ *
+ * @param red The red component of the sRGB color to create, in \([0..1]\)
+ * @param green The green component of the sRGB color to create, in \([0..1]\)
+ * @param blue The blue component of the sRGB color to create, in \([0..1]\)
+ *
+ * @return A color long
+ */
+ @ColorLong
+ public static long pack(float red, float green, float blue) {
+ return pack(red, green, blue, 1.0f, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+ /**
+ * Packs the sRGB color defined by the specified red, green, blue and alpha
+ * component values into an RGBA color long in the sRGB color space. See the
+ * documentation of this class for a description of the color long format.
+ *
+ * @param red The red component of the sRGB color to create, in \([0..1]\)
+ * @param green The green component of the sRGB color to create, in \([0..1]\)
+ * @param blue The blue component of the sRGB color to create, in \([0..1]\)
+ * @param alpha The alpha component of the sRGB color to create, in \([0..1]\)
+ *
+ * @return A color long
+ */
+ @ColorLong
+ public static long pack(float red, float green, float blue, float alpha) {
+ return pack(red, green, blue, alpha, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+ /**
+ * Packs the 3 component color defined by the specified red, green, blue and
+ * alpha component values into a color long in the specified color space. See the
+ * documentation of this class for a description of the color long format.
+ *
+ * The red, green and blue components must be in the range defined by the
+ * specified color space. See {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}.
+ *
+ * @param red The red component of the color to create
+ * @param green The green component of the color to create
+ * @param blue The blue component of the color to create
+ * @param alpha The alpha component of the color to create, in \([0..1]\)
+ *
+ * @return A color long
+ *
+ * @throws IllegalArgumentException If the color space's id is {@link ColorSpace#MIN_ID}
+ * or if the color space's color model has more than 3 components
+ */
+ @ColorLong
+ public static long pack(float red, float green, float blue, float alpha,
+ @NonNull ColorSpace colorSpace) {
+ if (colorSpace.isSrgb()) {
+ int argb =
+ ((int) (alpha * 255.0f + 0.5f) << 24) |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ return (argb & 0xffffffffL) << 32;
+ }
+ int id = colorSpace.getId();
+ if (id == ColorSpace.MIN_ID) {
+ throw new IllegalArgumentException(
+ "Unknown color space, please use a color space returned by ColorSpace.get()");
+ }
+ if (colorSpace.getComponentCount() > 3) {
+ throw new IllegalArgumentException(
+ "The color space must use a color model with at most 3 components");
+ }
+ @HalfFloat short r = Half.toHalf(red);
+ @HalfFloat short g = Half.toHalf(green);
+ @HalfFloat short b = Half.toHalf(blue);
+ int a = (int) (Math.max(0.0f, Math.min(alpha, 1.0f)) * 1023.0f + 0.5f);
+ // Suppress sign extension
+ return (r & 0xffffL) << 48 |
+ (g & 0xffffL) << 32 |
+ (b & 0xffffL) << 16 |
+ (a & 0x3ffL ) << 6 |
+ id & 0x3fL;
+ }
+ /**
+ * Converts the specified ARGB color int from the {@link ColorSpace.Named#SRGB sRGB}
+ * color space into the specified destination color space. The resulting color is
+ * returned as a color long. See the documentation of this class for a description
+ * of the color long format.
+ *
+ * @param color The sRGB color int to convert
+ * @param colorSpace The destination color space
+ * @return A color long in the destination color space
+ */
+ @ColorLong
+ public static long convert(@ColorInt int color, @NonNull ColorSpace colorSpace) {
+ float r = ((color >> 16) & 0xff) / 255.0f;
+ float g = ((color >> 8) & 0xff) / 255.0f;
+ float b = ((color ) & 0xff) / 255.0f;
+ float a = ((color >> 24) & 0xff) / 255.0f;
+ ColorSpace source = ColorSpace.get(ColorSpace.Named.SRGB);
+ return convert(r, g, b, a, source, colorSpace);
+ }
+ /**
+ * Converts the specified color long from its color space into the specified
+ * destination color space. The resulting color is returned as a color long. See
+ * the documentation of this class for a description of the color long format.
+ *
+ * When converting several colors in a row, it is recommended to use
+ * {@link #convert(long, ColorSpace.Connector)} instead to
+ * avoid the creation of a {@link ColorSpace.Connector} on every invocation.
+ *
+ * @param color The color long to convert
+ * @param colorSpace The destination color space
+ * @return A color long in the destination color space
+ * @throws IllegalArgumentException If the encoded color space is invalid or unknown
+ */
+ @ColorLong
+ public static long convert(@ColorLong long color, @NonNull ColorSpace colorSpace) {
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ ColorSpace source = colorSpace(color);
+ return convert(r, g, b, a, source, colorSpace);
+ }
+ /**
+ * Converts the specified 3 component color from the source color space to the
+ * destination color space. The resulting color is returned as a color long. See
+ * the documentation of this class for a description of the color long format.
+ *
+ * When converting multiple colors in a row, it is recommended to use
+ * {@link #convert(float, float, float, float, ColorSpace.Connector)} instead to
+ * avoid the creation of a {@link ColorSpace.Connector} on every invocation.
+ *
+ * The red, green and blue components must be in the range defined by the
+ * specified color space. See {@link ColorSpace#getMinValue(int)} and
+ * {@link ColorSpace#getMaxValue(int)}.
+ *
+ * @param r The red component of the color to convert
+ * @param g The green component of the color to convert
+ * @param b The blue component of the color to convert
+ * @param a The alpha component of the color to convert, in \([0..1]\)
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @return A color long in the destination color space
+ *
+ * @see #convert(float, float, float, float, ColorSpace.Connector)
+ */
+ @ColorLong
+ public static long convert(float r, float g, float b, float a,
+ @NonNull ColorSpace source, @NonNull ColorSpace destination) {
+ float[] c = ColorSpace.connect(source, destination).transform(r, g, b);
+ return pack(c[0], c[1], c[2], a, destination);
+ }
+ /**
+ * Converts the specified color long from a color space to another using the
+ * specified color space {@link ColorSpace.Connector connector}. The resulting
+ * color is returned as a color long. See the documentation of this class for a
+ * description of the color long format.
+ *
+ * When converting several colors in a row, this method is preferable to
+ * {@link #convert(long, ColorSpace)} as it prevents a new connector from being
+ * created on every invocation.
+ *
+ * The connector's source color space should match the color long's
+ * color space.
+ *
+ * @param color The color long to convert
+ * @param connector A color space connector, cannot be null
+ * @return A color long in the destination color space of the connector
+ */
+ @ColorLong
+ public static long convert(@ColorLong long color, @NonNull ColorSpace.Connector connector) {
+ float r = red(color);
+ float g = green(color);
+ float b = blue(color);
+ float a = alpha(color);
+ return convert(r, g, b, a, connector);
+ }
+ /**
+ * Converts the specified 3 component color from a color space to another using
+ * the specified color space {@link ColorSpace.Connector connector}. The resulting
+ * color is returned as a color long. See the documentation of this class for a
+ * description of the color long format.
+ *
+ * When converting several colors in a row, this method is preferable to
+ * {@link #convert(float, float, float, float, ColorSpace, ColorSpace)} as
+ * it prevents a new connector from being created on every invocation.
+ *
+ * The red, green and blue components must be in the range defined by the
+ * source color space of the connector. See {@link ColorSpace#getMinValue(int)}
+ * and {@link ColorSpace#getMaxValue(int)}.
+ *
+ * @param r The red component of the color to convert
+ * @param g The green component of the color to convert
+ * @param b The blue component of the color to convert
+ * @param a The alpha component of the color to convert, in \([0..1]\)
+ * @param connector A color space connector, cannot be null
+ * @return A color long in the destination color space of the connector
+ *
+ * @see #convert(float, float, float, float, ColorSpace, ColorSpace)
+ */
+ @ColorLong
+ public static long convert(float r, float g, float b, float a,
+ @NonNull ColorSpace.Connector connector) {
+ float[] c = connector.transform(r, g, b);
+ return pack(c[0], c[1], c[2], a, connector.getDestination());
+ }
+ /**
+ * Returns the relative luminance of a color.
+ *
+ * Based on the formula for relative luminance defined in WCAG 2.0,
+ * W3C Recommendation 11 December 2008.
+ *
+ * @return A value between 0 (darkest black) and 1 (lightest white)
+ *
+ * @throws IllegalArgumentException If the specified color's color space
+ * is unknown or does not use the {@link ColorSpace.Model#RGB RGB} color model
+ */
+ public static float luminance(@ColorLong long color) {
+ ColorSpace colorSpace = colorSpace(color);
+ if (colorSpace.getModel() != ColorSpace.Model.RGB) {
+ throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
+ "color space. The supplied color space is " + colorSpace.getModel());
+ }
+ DoubleUnaryOperator eotf = ((ColorSpace.Rgb) colorSpace).getEotf();
+ double r = eotf.applyAsDouble(red(color));
+ double g = eotf.applyAsDouble(green(color));
+ double b = eotf.applyAsDouble(blue(color));
+ return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
+ }
+ private static float saturate(float v) {
+ return v <= 0.0f ? 0.0f : (v >= 1.0f ? 1.0f : v);
+ }
+ /**
+ * Return the alpha component of a color int. This is the same as saying
+ * color >>> 24
+ */
+ @IntRange(from = 0, to = 255)
+ public static int alpha(int color) {
+ return color >>> 24;
+ }
+ /**
+ * Return the red component of a color int. This is the same as saying
+ * (color >> 16) & 0xFF
+ */
+ @IntRange(from = 0, to = 255)
+ public static int red(int color) {
+ return (color >> 16) & 0xFF;
+ }
+ /**
+ * Return the green component of a color int. This is the same as saying
+ * (color >> 8) & 0xFF
+ */
+ @IntRange(from = 0, to = 255)
+ public static int green(int color) {
+ return (color >> 8) & 0xFF;
+ }
+ /**
+ * Return the blue component of a color int. This is the same as saying
+ * color & 0xFF
+ */
+ @IntRange(from = 0, to = 255)
+ public static int blue(int color) {
+ return color & 0xFF;
+ }
+ /**
+ * Return a color-int from red, green, blue components.
+ * The alpha component is implicitly 255 (fully opaque).
+ * These component values should be \([0..255]\), but there is no
+ * range check performed, so if they are out of range, the
+ * returned color is undefined.
+ *
+ * @param red Red component \([0..255]\) of the color
+ * @param green Green component \([0..255]\) of the color
+ * @param blue Blue component \([0..255]\) of the color
+ */
+ @ColorInt
+ public static int rgb(
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue) {
+ return 0xff000000 | (red << 16) | (green << 8) | blue;
+ }
+ /**
+ * Return a color-int from red, green, blue float components
+ * in the range \([0..1]\). The alpha component is implicitly
+ * 1.0 (fully opaque). If the components are out of range, the
+ * returned color is undefined.
+ *
+ * @param red Red component \([0..1]\) of the color
+ * @param green Green component \([0..1]\) of the color
+ * @param blue Blue component \([0..1]\) of the color
+ */
+ @ColorInt
+ public static int rgb(float red, float green, float blue) {
+ return 0xff000000 |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ }
+ /**
+ * Return a color-int from alpha, red, green, blue components.
+ * These component values should be \([0..255]\), but there is no
+ * range check performed, so if they are out of range, the
+ * returned color is undefined.
+ * @param alpha Alpha component \([0..255]\) of the color
+ * @param red Red component \([0..255]\) of the color
+ * @param green Green component \([0..255]\) of the color
+ * @param blue Blue component \([0..255]\) of the color
+ */
+ @ColorInt
+ public static int argb(
+ @IntRange(from = 0, to = 255) int alpha,
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue) {
+ return (alpha << 24) | (red << 16) | (green << 8) | blue;
+ }
+ /**
+ * Return a color-int from alpha, red, green, blue float components
+ * in the range \([0..1]\). If the components are out of range, the
+ * returned color is undefined.
+ *
+ * @param alpha Alpha component \([0..1]\) of the color
+ * @param red Red component \([0..1]\) of the color
+ * @param green Green component \([0..1]\) of the color
+ * @param blue Blue component \([0..1]\) of the color
+ */
+ @ColorInt
+ public static int argb(float alpha, float red, float green, float blue) {
+ return ((int) (alpha * 255.0f + 0.5f) << 24) |
+ ((int) (red * 255.0f + 0.5f) << 16) |
+ ((int) (green * 255.0f + 0.5f) << 8) |
+ (int) (blue * 255.0f + 0.5f);
+ }
+ /**
+ * Returns the relative luminance of a color.
+ *
+ * Assumes sRGB encoding. Based on the formula for relative luminance
+ * defined in WCAG 2.0, W3C Recommendation 11 December 2008.
+ *
+ * @return a value between 0 (darkest black) and 1 (lightest white)
+ */
+ public static float luminance(@ColorInt int color) {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+ DoubleUnaryOperator eotf = cs.getEotf();
+ double r = eotf.applyAsDouble(red(color) / 255.0);
+ double g = eotf.applyAsDouble(green(color) / 255.0);
+ double b = eotf.applyAsDouble(blue(color) / 255.0);
+ return (float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b));
+ }
+ /**
+ *
Parse the color string, and return the corresponding color-int.
+ * If the string cannot be parsed, throws an IllegalArgumentException
+ * exception. Supported formats are:
+ *
+ *
+ * #RRGGBB
+ * #AARRGGBB
+ *
+ *
+ * The following names are also accepted: red
, blue
,
+ * green
, black
, white
, gray
,
+ * cyan
, magenta
, yellow
, lightgray
,
+ * darkgray
, grey
, lightgrey
, darkgrey
,
+ * aqua
, fuchsia
, lime
, maroon
,
+ * navy
, olive
, purple
, silver
,
+ * and teal
.
+ */
+ @ColorInt
+ public static int parseColor(@Size(min=1) String colorString) {
+ if (colorString.charAt(0) == '#') {
+ // Use a long to avoid rollovers on #ffXXXXXX
+ long color = Long.parseLong(colorString.substring(1), 16);
+ if (colorString.length() == 7) {
+ // Set the alpha value
+ color |= 0x00000000ff000000;
+ } else if (colorString.length() != 9) {
+ throw new IllegalArgumentException("Unknown color");
+ }
+ return (int)color;
+ } else {
+ Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException("Unknown color");
+ }
+ /**
+ * Convert RGB components to HSV.
+ *
+ * hsv[0]
is Hue \([0..360[\)
+ * hsv[1]
is Saturation \([0...1]\)
+ * hsv[2]
is Value \([0...1]\)
+ *
+ * @param red red component value \([0..255]\)
+ * @param green green component value \([0..255]\)
+ * @param blue blue component value \([0..255]\)
+ * @param hsv 3 element array which holds the resulting HSV components.
+ */
+ public static void RGBToHSV(
+ @IntRange(from = 0, to = 255) int red,
+ @IntRange(from = 0, to = 255) int green,
+ @IntRange(from = 0, to = 255) int blue, @Size(3) float hsv[]) {
+ if (hsv.length < 3) {
+ throw new RuntimeException("3 components required for hsv");
+ }
+ nativeRGBToHSV(red, green, blue, hsv);
+ }
+ /**
+ * Convert the ARGB color to its HSV components.
+ *
+ * hsv[0]
is Hue \([0..360[\)
+ * hsv[1]
is Saturation \([0...1]\)
+ * hsv[2]
is Value \([0...1]\)
+ *
+ * @param color the argb color to convert. The alpha component is ignored.
+ * @param hsv 3 element array which holds the resulting HSV components.
+ */
+ public static void colorToHSV(@ColorInt int color, @Size(3) float hsv[]) {
+ RGBToHSV((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, hsv);
+ }
+ /**
+ * Convert HSV components to an ARGB color. Alpha set to 0xFF.
+ *
+ * hsv[0]
is Hue \([0..360[\)
+ * hsv[1]
is Saturation \([0...1]\)
+ * hsv[2]
is Value \([0...1]\)
+ *
+ * If hsv values are out of range, they are pinned.
+ * @param hsv 3 element array which holds the input HSV components.
+ * @return the resulting argb color
+ */
+ @ColorInt
+ public static int HSVToColor(@Size(3) float hsv[]) {
+ return HSVToColor(0xFF, hsv);
+ }
+ /**
+ * Convert HSV components to an ARGB color. The alpha component is passed
+ * through unchanged.
+ *
+ * hsv[0]
is Hue \([0..360[\)
+ * hsv[1]
is Saturation \([0...1]\)
+ * hsv[2]
is Value \([0...1]\)
+ *
+ * If hsv values are out of range, they are pinned.
+ * @param alpha the alpha component of the returned argb color.
+ * @param hsv 3 element array which holds the input HSV components.
+ * @return the resulting argb color
+ */
+ @ColorInt
+ public static int HSVToColor(@IntRange(from = 0, to = 255) int alpha, @Size(3) float hsv[]) {
+ if (hsv.length < 3) {
+ throw new RuntimeException("3 components required for hsv");
+ }
+ return nativeHSVToColor(alpha, hsv);
+ }
+ private static native void nativeRGBToHSV(int red, int greed, int blue, float hsv[]);
+ private static native int nativeHSVToColor(int alpha, float hsv[]);
+ private static final HashMap sColorNameMap;
+ static {
+ sColorNameMap = new HashMap<>();
+ sColorNameMap.put("black", BLACK);
+ sColorNameMap.put("darkgray", DKGRAY);
+ sColorNameMap.put("gray", GRAY);
+ sColorNameMap.put("lightgray", LTGRAY);
+ sColorNameMap.put("white", WHITE);
+ sColorNameMap.put("red", RED);
+ sColorNameMap.put("green", GREEN);
+ sColorNameMap.put("blue", BLUE);
+ sColorNameMap.put("yellow", YELLOW);
+ sColorNameMap.put("cyan", CYAN);
+ sColorNameMap.put("magenta", MAGENTA);
+ sColorNameMap.put("aqua", 0xFF00FFFF);
+ sColorNameMap.put("fuchsia", 0xFFFF00FF);
+ sColorNameMap.put("darkgrey", DKGRAY);
+ sColorNameMap.put("grey", GRAY);
+ sColorNameMap.put("lightgrey", LTGRAY);
+ sColorNameMap.put("lime", 0xFF00FF00);
+ sColorNameMap.put("maroon", 0xFF800000);
+ sColorNameMap.put("navy", 0xFF000080);
+ sColorNameMap.put("olive", 0xFF808000);
+ sColorNameMap.put("purple", 0xFF800080);
+ sColorNameMap.put("silver", 0xFFC0C0C0);
+ sColorNameMap.put("teal", 0xFF008080);
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/android/graphics/ColorSpace.java b/Vision/src/main/java/android/graphics/ColorSpace.java
new file mode 100644
index 00000000..15ead4d0
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/ColorSpace.java
@@ -0,0 +1,3642 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.graphics;
+
+import android.annotation.*;
+import android.hardware.DataSpace;
+import android.hardware.DataSpace.NamedDataSpace;
+import android.util.SparseIntArray;
+
+import java.util.Arrays;
+import java.util.function.DoubleUnaryOperator;
+/**
+ * {@usesMathJax}
+ *
+ * A {@link ColorSpace} is used to identify a specific organization of colors.
+ * Each color space is characterized by a {@link Model color model} that defines
+ * how a color value is represented (for instance the {@link Model#RGB RGB} color
+ * model defines a color value as a triplet of numbers).
+ *
+ * Each component of a color must fall within a valid range, specific to each
+ * color space, defined by {@link #getMinValue(int)} and {@link #getMaxValue(int)}
+ * This range is commonly \([0..1]\). While it is recommended to use values in the
+ * valid range, a color space always clamps input and output values when performing
+ * operations such as converting to a different color space.
+ *
+ * Using color spaces
+ *
+ * This implementation provides a pre-defined set of common color spaces
+ * described in the {@link Named} enum. To obtain an instance of one of the
+ * pre-defined color spaces, simply invoke {@link #get(Named)}:
+ *
+ *
+ * ColorSpace sRgb = ColorSpace.get(ColorSpace.Named.SRGB);
+ *
+ *
+ * The {@link #get(Named)} method always returns the same instance for a given
+ * name. Color spaces with an {@link Model#RGB RGB} color model can be safely
+ * cast to {@link Rgb}. Doing so gives you access to more APIs to query various
+ * properties of RGB color models: color gamut primaries, transfer functions,
+ * conversions to and from linear space, etc. Please refer to {@link Rgb} for
+ * more information.
+ *
+ * The documentation of {@link Named} provides a detailed description of the
+ * various characteristics of each available color space.
+ *
+ * Color space conversions
+ * To allow conversion between color spaces, this implementation uses the CIE
+ * XYZ profile connection space (PCS). Color values can be converted to and from
+ * this PCS using {@link #toXyz(float[])} and {@link #fromXyz(float[])}.
+ *
+ * For color space with a non-RGB color model, the white point of the PCS
+ * must be the CIE standard illuminant D50. RGB color spaces use their
+ * native white point (D65 for {@link Named#SRGB sRGB} for instance and must
+ * undergo {@link Adaptation chromatic adaptation} as necessary.
+ *
+ * Since the white point of the PCS is not defined for RGB color space, it is
+ * highly recommended to use the variants of the {@link #connect(ColorSpace, ColorSpace)}
+ * method to perform conversions between color spaces. A color space can be
+ * manually adapted to a specific white point using {@link #adapt(ColorSpace, float[])}.
+ * Please refer to the documentation of {@link Rgb RGB color spaces} for more
+ * information. Several common CIE standard illuminants are provided in this
+ * class as reference (see {@link #ILLUMINANT_D65} or {@link #ILLUMINANT_D50}
+ * for instance).
+ *
+ * Here is an example of how to convert from a color space to another:
+ *
+ *
+ * // Convert from DCI-P3 to Rec.2020
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.DCI_P3),
+ * ColorSpace.get(ColorSpace.Named.BT2020));
+ *
+ * float[] bt2020 = connector.transform(p3r, p3g, p3b);
+ *
+ *
+ * You can easily convert to {@link Named#SRGB sRGB} by omitting the second
+ * parameter:
+ *
+ *
+ * // Convert from DCI-P3 to sRGB
+ * ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3));
+ *
+ * float[] sRGB = connector.transform(p3r, p3g, p3b);
+ *
+ *
+ * Conversions also work between color spaces with different color models:
+ *
+ *
+ * // Convert from CIE L*a*b* (color model Lab) to Rec.709 (color model RGB)
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.CIE_LAB),
+ * ColorSpace.get(ColorSpace.Named.BT709));
+ *
+ *
+ * Color spaces and multi-threading
+ *
+ * Color spaces and other related classes ({@link Connector} for instance)
+ * are immutable and stateless. They can be safely used from multiple concurrent
+ * threads.
+ *
+ * Public static methods provided by this class, such as {@link #get(Named)}
+ * and {@link #connect(ColorSpace, ColorSpace)}, are also guaranteed to be
+ * thread-safe.
+ *
+ * @see #get(Named)
+ * @see Named
+ * @see Model
+ * @see Connector
+ * @see Adaptation
+ */
+@AnyThread
+@SuppressWarnings("StaticInitializerReferencesSubClass")
+@SuppressAutoDoc
+public abstract class ColorSpace {
+ /**
+ * Standard CIE 1931 2° illuminant A, encoded in xyY.
+ * This illuminant has a color temperature of 2856K.
+ */
+ public static final float[] ILLUMINANT_A = { 0.44757f, 0.40745f };
+ /**
+ * Standard CIE 1931 2° illuminant B, encoded in xyY.
+ * This illuminant has a color temperature of 4874K.
+ */
+ public static final float[] ILLUMINANT_B = { 0.34842f, 0.35161f };
+ /**
+ * Standard CIE 1931 2° illuminant C, encoded in xyY.
+ * This illuminant has a color temperature of 6774K.
+ */
+ public static final float[] ILLUMINANT_C = { 0.31006f, 0.31616f };
+ /**
+ * Standard CIE 1931 2° illuminant D50, encoded in xyY.
+ * This illuminant has a color temperature of 5003K. This illuminant
+ * is used by the profile connection space in ICC profiles.
+ */
+ public static final float[] ILLUMINANT_D50 = { 0.34567f, 0.35850f };
+ /**
+ * Standard CIE 1931 2° illuminant D55, encoded in xyY.
+ * This illuminant has a color temperature of 5503K.
+ */
+ public static final float[] ILLUMINANT_D55 = { 0.33242f, 0.34743f };
+ /**
+ * Standard CIE 1931 2° illuminant D60, encoded in xyY.
+ * This illuminant has a color temperature of 6004K.
+ */
+ public static final float[] ILLUMINANT_D60 = { 0.32168f, 0.33767f };
+ /**
+ * Standard CIE 1931 2° illuminant D65, encoded in xyY.
+ * This illuminant has a color temperature of 6504K. This illuminant
+ * is commonly used in RGB color spaces such as sRGB, BT.709, etc.
+ */
+ public static final float[] ILLUMINANT_D65 = { 0.31271f, 0.32902f };
+ /**
+ * Standard CIE 1931 2° illuminant D75, encoded in xyY.
+ * This illuminant has a color temperature of 7504K.
+ */
+ public static final float[] ILLUMINANT_D75 = { 0.29902f, 0.31485f };
+ /**
+ * Standard CIE 1931 2° illuminant E, encoded in xyY.
+ * This illuminant has a color temperature of 5454K.
+ */
+ public static final float[] ILLUMINANT_E = { 0.33333f, 0.33333f };
+ /**
+ * The minimum ID value a color space can have.
+ *
+ * @see #getId()
+ */
+ public static final int MIN_ID = -1; // Do not change
+ /**
+ * The maximum ID value a color space can have.
+ *
+ * @see #getId()
+ */
+ public static final int MAX_ID = 63; // Do not change, used to encode in longs
+ private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f };
+ private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f };
+ /**
+ * A gray color space does not have meaningful primaries, so we use this arbitrary set.
+ */
+ private static final float[] GRAY_PRIMARIES = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
+ private static final float[] ILLUMINANT_D50_XYZ = { 0.964212f, 1.0f, 0.825188f };
+ private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4);
+ // See static initialization block next to #get(Named)
+ private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length];
+ private static final SparseIntArray sDataToColorSpaces = new SparseIntArray();
+ @NonNull private final String mName;
+ @NonNull private final Model mModel;
+ @IntRange(from = MIN_ID, to = MAX_ID) private final int mId;
+ /**
+ * {@usesMathJax}
+ *
+ * List of common, named color spaces. A corresponding instance of
+ * {@link ColorSpace} can be obtained by calling {@link ColorSpace#get(Named)}:
+ *
+ *
+ * ColorSpace cs = ColorSpace.get(ColorSpace.Named.DCI_P3);
+ *
+ *
+ * The properties of each color space are described below (see {@link #SRGB sRGB}
+ * for instance). When applicable, the color gamut of each color space is compared
+ * to the color gamut of sRGB using a CIE 1931 xy chromaticity diagram. This diagram
+ * shows the location of the color space's primaries and white point.
+ *
+ * @see ColorSpace#get(Named)
+ */
+ public enum Named {
+ // NOTE: Do NOT change the order of the enum
+ /**
+ * {@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.640 | 0.300 | 0.150 | 0.3127 |
+ * y | 0.330 | 0.600 | 0.060 | 0.3290 |
+ * Property | Value |
+ * Name | sRGB IEC61966-2.1 |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{sRGB} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0031308 \\\
+ * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0031308 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{sRGB}}{12.92} & C_{sRGB} \lt 0.04045 \\\
+ * \left( \frac{C_{sRGB} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * sRGB
+ *
+ */
+ SRGB,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space sRGB standardized as IEC 61966-2.1:1999.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.640 | 0.300 | 0.150 | 0.3127 |
+ * y | 0.330 | 0.600 | 0.060 | 0.3290 |
+ * Property | Value |
+ * Name | sRGB IEC61966-2.1 (Linear) |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(C_{sRGB} = C_{linear}\) |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(C_{linear} = C_{sRGB}\) |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * sRGB
+ *
+ */
+ LINEAR_SRGB,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space scRGB-nl standardized as IEC 61966-2-2:2003.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.640 | 0.300 | 0.150 | 0.3127 |
+ * y | 0.330 | 0.600 | 0.060 | 0.3290 |
+ * Property | Value |
+ * Name | scRGB-nl IEC 61966-2-2:2003 |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{scRGB} = \begin{cases} sign(C_{linear}) 12.92 \times \left| C_{linear} \right| &
+ * \left| C_{linear} \right| \lt 0.0031308 \\\
+ * sign(C_{linear}) 1.055 \times \left| C_{linear} \right| ^{\frac{1}{2.4}} - 0.055 &
+ * \left| C_{linear} \right| \ge 0.0031308 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}sign(C_{scRGB}) \frac{\left| C_{scRGB} \right|}{12.92} &
+ * \left| C_{scRGB} \right| \lt 0.04045 \\\
+ * sign(C_{scRGB}) \left( \frac{\left| C_{scRGB} \right| + 0.055}{1.055} \right) ^{2.4} &
+ * \left| C_{scRGB} \right| \ge 0.04045 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([-0.799..2.399[\) |
+ *
+ *
+ *
+ * Extended sRGB (orange) vs sRGB (white)
+ *
+ */
+ EXTENDED_SRGB,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space scRGB standardized as IEC 61966-2-2:2003.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.640 | 0.300 | 0.150 | 0.3127 |
+ * y | 0.330 | 0.600 | 0.060 | 0.3290 |
+ * Property | Value |
+ * Name | scRGB IEC 61966-2-2:2003 |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(C_{scRGB} = C_{linear}\) |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(C_{linear} = C_{scRGB}\) |
+ *
+ * Range | \([-0.5..7.499[\) |
+ *
+ *
+ *
+ * Extended sRGB (orange) vs sRGB (white)
+ *
+ */
+ LINEAR_EXTENDED_SRGB,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space BT.709 standardized as Rec. ITU-R BT.709-5.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.640 | 0.300 | 0.150 | 0.3127 |
+ * y | 0.330 | 0.600 | 0.060 | 0.3290 |
+ * Property | Value |
+ * Name | Rec. ITU-R BT.709-5 |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\\
+ * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\\
+ * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * BT.709
+ *
+ */
+ BT709,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space BT.2020 standardized as Rec. ITU-R BT.2020-1.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.708 | 0.170 | 0.131 | 0.3127 |
+ * y | 0.292 | 0.797 | 0.046 | 0.3290 |
+ * Property | Value |
+ * Name | Rec. ITU-R BT.2020-1 |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{BT2020} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.0181 \\\
+ * 1.0993 \times C_{linear}^{\frac{1}{2.2}} - 0.0993 & C_{linear} \ge 0.0181 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT2020}}{4.5} & C_{BT2020} \lt 0.08145 \\\
+ * \left( \frac{C_{BT2020} + 0.0993}{1.0993} \right) ^{2.2} & C_{BT2020} \ge 0.08145 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * BT.2020 (orange) vs sRGB (white)
+ *
+ */
+ BT2020,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space DCI-P3 standardized as SMPTE RP 431-2-2007.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.680 | 0.265 | 0.150 | 0.314 |
+ * y | 0.320 | 0.690 | 0.060 | 0.351 |
+ * Property | Value |
+ * Name | SMPTE RP 431-2-2007 DCI (P3) |
+ * CIE standard illuminant | N/A |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(C_{P3} = C_{linear}^{\frac{1}{2.6}}\) |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(C_{linear} = C_{P3}^{2.6}\) |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * DCI-P3 (orange) vs sRGB (white)
+ *
+ */
+ DCI_P3,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space Display P3 based on SMPTE RP 431-2-2007 and IEC 61966-2.1:1999.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.680 | 0.265 | 0.150 | 0.3127 |
+ * y | 0.320 | 0.690 | 0.060 | 0.3290 |
+ * Property | Value |
+ * Name | Display P3 |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{DisplayP3} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0030186 \\\
+ * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0030186 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{DisplayP3}}{12.92} & C_{sRGB} \lt 0.04045 \\\
+ * \left( \frac{C_{DisplayP3} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * Display P3 (orange) vs sRGB (white)
+ *
+ */
+ DISPLAY_P3,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space NTSC, 1953 standard.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.67 | 0.21 | 0.14 | 0.310 |
+ * y | 0.33 | 0.71 | 0.08 | 0.316 |
+ * Property | Value |
+ * Name | NTSC (1953) |
+ * CIE standard illuminant | C |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\\
+ * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\\
+ * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * NTSC 1953 (orange) vs sRGB (white)
+ *
+ */
+ NTSC_1953,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space SMPTE C.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.630 | 0.310 | 0.155 | 0.3127 |
+ * y | 0.340 | 0.595 | 0.070 | 0.3290 |
+ * Property | Value |
+ * Name | SMPTE-C RGB |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{BT709} = \begin{cases} 4.5 \times C_{linear} & C_{linear} \lt 0.018 \\\
+ * 1.099 \times C_{linear}^{\frac{1}{2.2}} - 0.099 & C_{linear} \ge 0.018 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{BT709}}{4.5} & C_{BT709} \lt 0.081 \\\
+ * \left( \frac{C_{BT709} + 0.099}{1.099} \right) ^{2.2} & C_{BT709} \ge 0.081 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * SMPTE-C (orange) vs sRGB (white)
+ *
+ */
+ SMPTE_C,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space Adobe RGB (1998).
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.64 | 0.21 | 0.15 | 0.3127 |
+ * y | 0.33 | 0.71 | 0.06 | 0.3290 |
+ * Property | Value |
+ * Name | Adobe RGB (1998) |
+ * CIE standard illuminant | D65 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(C_{RGB} = C_{linear}^{\frac{1}{2.2}}\) |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(C_{linear} = C_{RGB}^{2.2}\) |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * Adobe RGB (orange) vs sRGB (white)
+ *
+ */
+ ADOBE_RGB,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space ProPhoto RGB standardized as ROMM RGB ISO 22028-2:2013.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.7347 | 0.1596 | 0.0366 | 0.3457 |
+ * y | 0.2653 | 0.8404 | 0.0001 | 0.3585 |
+ * Property | Value |
+ * Name | ROMM RGB ISO 22028-2:2013 |
+ * CIE standard illuminant | D50 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(\begin{equation}
+ * C_{ROMM} = \begin{cases} 16 \times C_{linear} & C_{linear} \lt 0.001953 \\\
+ * C_{linear}^{\frac{1}{1.8}} & C_{linear} \ge 0.001953 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(\begin{equation}
+ * C_{linear} = \begin{cases}\frac{C_{ROMM}}{16} & C_{ROMM} \lt 0.031248 \\\
+ * C_{ROMM}^{1.8} & C_{ROMM} \ge 0.031248 \end{cases}
+ * \end{equation}\)
+ * |
+ *
+ * Range | \([0..1]\) |
+ *
+ *
+ *
+ * ProPhoto RGB (orange) vs sRGB (white)
+ *
+ */
+ PRO_PHOTO_RGB,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space ACES standardized as SMPTE ST 2065-1:2012.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.73470 | 0.00000 | 0.00010 | 0.32168 |
+ * y | 0.26530 | 1.00000 | -0.07700 | 0.33767 |
+ * Property | Value |
+ * Name | SMPTE ST 2065-1:2012 ACES |
+ * CIE standard illuminant | D60 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(C_{ACES} = C_{linear}\) |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(C_{linear} = C_{ACES}\) |
+ *
+ * Range | \([-65504.0, 65504.0]\) |
+ *
+ *
+ *
+ * ACES (orange) vs sRGB (white)
+ *
+ */
+ ACES,
+ /**
+ * {@link ColorSpace.Rgb RGB} color space ACEScg standardized as Academy S-2014-004.
+ *
+ *
+ * Chromaticity | Red | Green | Blue | White point |
+ *
+ * x | 0.713 | 0.165 | 0.128 | 0.32168 |
+ * y | 0.293 | 0.830 | 0.044 | 0.33767 |
+ * Property | Value |
+ * Name | Academy S-2014-004 ACEScg |
+ * CIE standard illuminant | D60 |
+ *
+ * Opto-electronic transfer function (OETF) |
+ * \(C_{ACEScg} = C_{linear}\) |
+ *
+ *
+ * Electro-optical transfer function (EOTF) |
+ * \(C_{linear} = C_{ACEScg}\) |
+ *
+ * Range | \([-65504.0, 65504.0]\) |
+ *
+ *
+ *
+ * ACEScg (orange) vs sRGB (white)
+ *
+ */
+ ACESCG,
+ /**
+ * {@link Model#XYZ XYZ} color space CIE XYZ. This color space assumes standard
+ * illuminant D50 as its white point.
+ *
+ * Property | Value |
+ * Name | Generic XYZ |
+ * CIE standard illuminant | D50 |
+ * Range | \([-2.0, 2.0]\) |
+ *
+ */
+ CIE_XYZ,
+ /**
+ * {@link Model#LAB Lab} color space CIE L*a*b*. This color space uses CIE XYZ D50
+ * as a profile conversion space.
+ *
+ * Property | Value |
+ * Name | Generic L*a*b* |
+ * CIE standard illuminant | D50 |
+ * Range | \(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\) |
+ *
+ */
+ CIE_LAB
+ // Update the initialization block next to #get(Named) when adding new values
+ }
+ /**
+ * A render intent determines how a {@link ColorSpace.Connector connector}
+ * maps colors from one color space to another. The choice of mapping is
+ * important when the source color space has a larger color gamut than the
+ * destination color space.
+ *
+ * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ public enum RenderIntent {
+ /**
+ * Compresses the source gamut into the destination gamut.
+ * This render intent affects all colors, inside and outside
+ * of destination gamut. The goal of this render intent is
+ * to preserve the visual relationship between colors.
+ *
+ * This render intent is currently not
+ * implemented and behaves like {@link #RELATIVE}.
+ */
+ PERCEPTUAL,
+ /**
+ * Similar to the {@link #ABSOLUTE} render intent, this render
+ * intent matches the closest color in the destination gamut
+ * but makes adjustments for the destination white point.
+ */
+ RELATIVE,
+ /**
+ * Attempts to maintain the relative saturation of colors
+ * from the source gamut to the destination gamut, to keep
+ * highly saturated colors as saturated as possible.
+ *
+ * This render intent is currently not
+ * implemented and behaves like {@link #RELATIVE}.
+ */
+ SATURATION,
+ /**
+ * Colors that are in the destination gamut are left unchanged.
+ * Colors that fall outside of the destination gamut are mapped
+ * to the closest possible color within the gamut of the destination
+ * color space (they are clipped).
+ */
+ ABSOLUTE
+ }
+ /**
+ * {@usesMathJax}
+ *
+ * List of adaptation matrices that can be used for chromatic adaptation
+ * using the von Kries transform. These matrices are used to convert values
+ * in the CIE XYZ space to values in the LMS space (Long Medium Short).
+ *
+ * Given an adaptation matrix \(A\), the conversion from XYZ to
+ * LMS is straightforward:
+ *
+ * $$\left[ \begin{array}{c} L\\ M\\ S \end{array} \right] =
+ * A \left[ \begin{array}{c} X\\ Y\\ Z \end{array} \right]$$
+ *
+ * The complete von Kries transform \(T\) uses a diagonal matrix
+ * noted \(D\) to perform the adaptation in LMS space. In addition
+ * to \(A\) and \(D\), the source white point \(W1\) and the destination
+ * white point \(W2\) must be specified:
+ *
+ * $$\begin{align*}
+ * \left[ \begin{array}{c} L_1\\ M_1\\ S_1 \end{array} \right] &=
+ * A \left[ \begin{array}{c} W1_X\\ W1_Y\\ W1_Z \end{array} \right] \\\
+ * \left[ \begin{array}{c} L_2\\ M_2\\ S_2 \end{array} \right] &=
+ * A \left[ \begin{array}{c} W2_X\\ W2_Y\\ W2_Z \end{array} \right] \\\
+ * D &= \left[ \begin{matrix} \frac{L_2}{L_1} & 0 & 0 \\\
+ * 0 & \frac{M_2}{M_1} & 0 \\\
+ * 0 & 0 & \frac{S_2}{S_1} \end{matrix} \right] \\\
+ * T &= A^{-1}.D.A
+ * \end{align*}$$
+ *
+ * As an example, the resulting matrix \(T\) can then be used to
+ * perform the chromatic adaptation of sRGB XYZ transform from D65
+ * to D50:
+ *
+ * $$sRGB_{D50} = T.sRGB_{D65}$$
+ *
+ * @see ColorSpace.Connector
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ */
+ public enum Adaptation {
+ /**
+ * Bradford chromatic adaptation transform, as defined in the
+ * CIECAM97s color appearance model.
+ */
+ BRADFORD(new float[] {
+ 0.8951f, -0.7502f, 0.0389f,
+ 0.2664f, 1.7135f, -0.0685f,
+ -0.1614f, 0.0367f, 1.0296f
+ }),
+ /**
+ * von Kries chromatic adaptation transform.
+ */
+ VON_KRIES(new float[] {
+ 0.40024f, -0.22630f, 0.00000f,
+ 0.70760f, 1.16532f, 0.00000f,
+ -0.08081f, 0.04570f, 0.91822f
+ }),
+ /**
+ * CIECAT02 chromatic adaption transform, as defined in the
+ * CIECAM02 color appearance model.
+ */
+ CIECAT02(new float[] {
+ 0.7328f, -0.7036f, 0.0030f,
+ 0.4296f, 1.6975f, 0.0136f,
+ -0.1624f, 0.0061f, 0.9834f
+ });
+ final float[] mTransform;
+ Adaptation(@NonNull @Size(9) float[] transform) {
+ mTransform = transform;
+ }
+ }
+ /**
+ * A color model is required by a {@link ColorSpace} to describe the
+ * way colors can be represented as tuples of numbers. A common color
+ * model is the {@link #RGB RGB} color model which defines a color
+ * as represented by a tuple of 3 numbers (red, green and blue).
+ */
+ public enum Model {
+ /**
+ * The RGB model is a color model with 3 components that
+ * refer to the three additive primaries: red, green
+ * and blue.
+ */
+ RGB(3),
+ /**
+ * The XYZ model is a color model with 3 components that
+ * are used to model human color vision on a basic sensory
+ * level.
+ */
+ XYZ(3),
+ /**
+ * The Lab model is a color model with 3 components used
+ * to describe a color space that is more perceptually
+ * uniform than XYZ.
+ */
+ LAB(3),
+ /**
+ * The CMYK model is a color model with 4 components that
+ * refer to four inks used in color printing: cyan, magenta,
+ * yellow and black (or key). CMYK is a subtractive color
+ * model.
+ */
+ CMYK(4);
+ private final int mComponentCount;
+ Model(@IntRange(from = 1, to = 4) int componentCount) {
+ mComponentCount = componentCount;
+ }
+ /**
+ * Returns the number of components for this color model.
+ *
+ * @return An integer between 1 and 4
+ */
+ @IntRange(from = 1, to = 4)
+ public int getComponentCount() {
+ return mComponentCount;
+ }
+ }
+ /*package*/ ColorSpace(
+ @NonNull String name,
+ @NonNull Model model,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ if (name == null || name.length() < 1) {
+ throw new IllegalArgumentException("The name of a color space cannot be null and " +
+ "must contain at least 1 character");
+ }
+ if (model == null) {
+ throw new IllegalArgumentException("A color space must have a model");
+ }
+ if (id < MIN_ID || id > MAX_ID) {
+ throw new IllegalArgumentException("The id must be between " +
+ MIN_ID + " and " + MAX_ID);
+ }
+ mName = name;
+ mModel = model;
+ mId = id;
+ }
+ /**
+ * Returns the name of this color space. The name is never null
+ * and contains always at least 1 character.
+ *
+ * Color space names are recommended to be unique but are not
+ * guaranteed to be. There is no defined format but the name usually
+ * falls in one of the following categories:
+ *
+ * - Generic names used to identify color spaces in non-RGB
+ * color models. For instance: {@link Named#CIE_LAB Generic L*a*b*}.
+ * - Names tied to a particular specification. For instance:
+ * {@link Named#SRGB sRGB IEC61966-2.1} or
+ * {@link Named#ACES SMPTE ST 2065-1:2012 ACES}.
+ * - Ad-hoc names, often generated procedurally or by the user
+ * during a calibration workflow. These names often contain the
+ * make and model of the display.
+ *
+ *
+ * Because the format of color space names is not defined, it is
+ * not recommended to programmatically identify a color space by its
+ * name alone. Names can be used as a first approximation.
+ *
+ * It is however perfectly acceptable to display color space names to
+ * users in a UI, or in debuggers and logs. When displaying a color space
+ * name to the user, it is recommended to add extra information to avoid
+ * ambiguities: color model, a representation of the color space's gamut,
+ * white point, etc.
+ *
+ * @return A non-null String of length >= 1
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+ /**
+ * Returns the ID of this color space. Positive IDs match the color
+ * spaces enumerated in {@link Named}. A negative ID indicates a
+ * color space created by calling one of the public constructors.
+ *
+ * @return An integer between {@link #MIN_ID} and {@link #MAX_ID}
+ */
+ @IntRange(from = MIN_ID, to = MAX_ID)
+ public int getId() {
+ return mId;
+ }
+ /**
+ * Return the color model of this color space.
+ *
+ * @return A non-null {@link Model}
+ *
+ * @see Model
+ * @see #getComponentCount()
+ */
+ @NonNull
+ public Model getModel() {
+ return mModel;
+ }
+ /**
+ * Returns the number of components that form a color value according
+ * to this color space's color model.
+ *
+ * @return An integer between 1 and 4
+ *
+ * @see Model
+ * @see #getModel()
+ */
+ @IntRange(from = 1, to = 4)
+ public int getComponentCount() {
+ return mModel.getComponentCount();
+ }
+ /**
+ * Returns whether this color space is a wide-gamut color space.
+ * An RGB color space is wide-gamut if its gamut entirely contains
+ * the {@link Named#SRGB sRGB} gamut and if the area of its gamut is
+ * 90% of greater than the area of the {@link Named#NTSC_1953 NTSC}
+ * gamut.
+ *
+ * @return True if this color space is a wide-gamut color space,
+ * false otherwise
+ */
+ public abstract boolean isWideGamut();
+ /**
+ * Indicates whether this color space is the sRGB color space or
+ * equivalent to the sRGB color space.
+ * A color space is considered sRGB if it meets all the following
+ * conditions:
+ *
+ * - Its color model is {@link Model#RGB}.
+ * -
+ * Its primaries are within 1e-3 of the true
+ * {@link Named#SRGB sRGB} primaries.
+ *
+ * -
+ * Its white point is within 1e-3 of the CIE standard
+ * illuminant {@link #ILLUMINANT_D65 D65}.
+ *
+ * - Its opto-electronic transfer function is not linear.
+ * - Its electro-optical transfer function is not linear.
+ * - Its transfer functions yield values within 1e-3 of {@link Named#SRGB}.
+ * - Its range is \([0..1]\).
+ *
+ * This method always returns true for {@link Named#SRGB}.
+ *
+ * @return True if this color space is the sRGB color space (or a
+ * close approximation), false otherwise
+ */
+ public boolean isSrgb() {
+ return false;
+ }
+ /**
+ * Returns the minimum valid value for the specified component of this
+ * color space's color model.
+ *
+ * @param component The index of the component
+ * @return A floating point value less than {@link #getMaxValue(int)}
+ *
+ * @see #getMaxValue(int)
+ * @see Model#getComponentCount()
+ */
+ public abstract float getMinValue(@IntRange(from = 0, to = 3) int component);
+ /**
+ * Returns the maximum valid value for the specified component of this
+ * color space's color model.
+ *
+ * @param component The index of the component
+ * @return A floating point value greater than {@link #getMinValue(int)}
+ *
+ * @see #getMinValue(int)
+ * @see Model#getComponentCount()
+ */
+ public abstract float getMaxValue(@IntRange(from = 0, to = 3) int component);
+ /**
+ * Converts a color value from this color space's model to
+ * tristimulus CIE XYZ values. If the color model of this color
+ * space is not {@link Model#RGB RGB}, it is assumed that the
+ * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50}
+ * standard illuminant.
+ *
+ * This method is a convenience for color spaces with a model
+ * of 3 components ({@link Model#RGB RGB} or {@link Model#LAB}
+ * for instance). With color spaces using fewer or more components,
+ * use {@link #toXyz(float[])} instead
.
+ *
+ * @param r The first component of the value to convert from (typically R in RGB)
+ * @param g The second component of the value to convert from (typically G in RGB)
+ * @param b The third component of the value to convert from (typically B in RGB)
+ * @return A new array of 3 floats, containing tristimulus XYZ values
+ *
+ * @see #toXyz(float[])
+ * @see #fromXyz(float, float, float)
+ */
+ @NonNull
+ @Size(3)
+ public float[] toXyz(float r, float g, float b) {
+ return toXyz(new float[] { r, g, b });
+ }
+ /**
+ * Converts a color value from this color space's model to
+ * tristimulus CIE XYZ values. If the color model of this color
+ * space is not {@link Model#RGB RGB}, it is assumed that the
+ * target CIE XYZ space uses a {@link #ILLUMINANT_D50 D50}
+ * standard illuminant.
+ *
+ * The specified array's length must be at least
+ * equal to to the number of color components as returned by
+ * {@link Model#getComponentCount()}.
+ *
+ * @param v An array of color components containing the color space's
+ * color value to convert to XYZ, and large enough to hold
+ * the resulting tristimulus XYZ values
+ * @return The array passed in parameter
+ *
+ * @see #toXyz(float, float, float)
+ * @see #fromXyz(float[])
+ */
+ @NonNull
+ @Size(min = 3)
+ public abstract float[] toXyz(@NonNull @Size(min = 3) float[] v);
+ /**
+ * Converts tristimulus values from the CIE XYZ space to this
+ * color space's color model.
+ *
+ * @param x The X component of the color value
+ * @param y The Y component of the color value
+ * @param z The Z component of the color value
+ * @return A new array whose size is equal to the number of color
+ * components as returned by {@link Model#getComponentCount()}
+ *
+ * @see #fromXyz(float[])
+ * @see #toXyz(float, float, float)
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] fromXyz(float x, float y, float z) {
+ float[] xyz = new float[mModel.getComponentCount()];
+ xyz[0] = x;
+ xyz[1] = y;
+ xyz[2] = z;
+ return fromXyz(xyz);
+ }
+ /**
+ * Converts tristimulus values from the CIE XYZ space to this color
+ * space's color model. The resulting value is passed back in the specified
+ * array.
+ *
+ * The specified array's length must be at least equal to
+ * to the number of color components as returned by
+ * {@link Model#getComponentCount()}, and its first 3 values must
+ * be the XYZ components to convert from.
+ *
+ * @param v An array of color components containing the XYZ values
+ * to convert from, and large enough to hold the number
+ * of components of this color space's model
+ * @return The array passed in parameter
+ *
+ * @see #fromXyz(float, float, float)
+ * @see #toXyz(float[])
+ */
+ @NonNull
+ @Size(min = 3)
+ public abstract float[] fromXyz(@NonNull @Size(min = 3) float[] v);
+ /**
+ * Returns a string representation of the object. This method returns
+ * a string equal to the value of:
+ *
+ *
+ * getName() + "(id=" + getId() + ", model=" + getModel() + ")"
+ *
+ *
+ * For instance, the string representation of the {@link Named#SRGB sRGB}
+ * color space is equal to the following value:
+ *
+ *
+ * sRGB IEC61966-2.1 (id=0, model=RGB)
+ *
+ *
+ * @return A string representation of the object
+ */
+ @Override
+ @NonNull
+ public String toString() {
+ return mName + " (id=" + mId + ", model=" + mModel + ")";
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ColorSpace that = (ColorSpace) o;
+ if (mId != that.mId) return false;
+ //noinspection SimplifiableIfStatement
+ if (!mName.equals(that.mName)) return false;
+ return mModel == that.mModel;
+ }
+ @Override
+ public int hashCode() {
+ int result = mName.hashCode();
+ result = 31 * result + mModel.hashCode();
+ result = 31 * result + mId;
+ return result;
+ }
+ /**
+ * Connects two color spaces to allow conversion from the source color
+ * space to the destination color space. If the source and destination
+ * color spaces do not have the same profile connection space (CIE XYZ
+ * with the same white point), they are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ *
+ * If the source and destination are the same, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.
+ *
+ * Colors are mapped from the source color space to the destination color
+ * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.
+ *
+ * @param source The color space to convert colors from
+ * @param destination The color space to convert colors to
+ * @return A non-null connector between the two specified color spaces
+ *
+ * @see #connect(ColorSpace)
+ * @see #connect(ColorSpace, RenderIntent)
+ * @see #connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination) {
+ return connect(source, destination, RenderIntent.PERCEPTUAL);
+ }
+ /**
+ * Connects two color spaces to allow conversion from the source color
+ * space to the destination color space. If the source and destination
+ * color spaces do not have the same profile connection space (CIE XYZ
+ * with the same white point), they are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ *
+ * If the source and destination are the same, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.
+ *
+ * @param source The color space to convert colors from
+ * @param destination The color space to convert colors to
+ * @param intent The render intent to map colors from the source to the destination
+ * @return A non-null connector between the two specified color spaces
+ *
+ * @see #connect(ColorSpace)
+ * @see #connect(ColorSpace, RenderIntent)
+ * @see #connect(ColorSpace, ColorSpace)
+ */
+ @NonNull
+ @SuppressWarnings("ConstantConditions")
+ public static Connector connect(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ if (source.equals(destination)) return Connector.identity(source);
+ if (source.getModel() == Model.RGB && destination.getModel() == Model.RGB) {
+ return new Connector.Rgb((Rgb) source, (Rgb) destination, intent);
+ }
+ return new Connector(source, destination, intent);
+ }
+ /**
+ * Connects the specified color spaces to sRGB.
+ * If the source color space does not use CIE XYZ D65 as its profile
+ * connection space, the two spaces are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ *
+ * If the source is the sRGB color space, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.
+ *
+ * Colors are mapped from the source color space to the destination color
+ * space using the {@link RenderIntent#PERCEPTUAL perceptual} render intent.
+ *
+ * @param source The color space to convert colors from
+ * @return A non-null connector between the specified color space and sRGB
+ *
+ * @see #connect(ColorSpace, RenderIntent)
+ * @see #connect(ColorSpace, ColorSpace)
+ * @see #connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source) {
+ return connect(source, RenderIntent.PERCEPTUAL);
+ }
+ /**
+ * Connects the specified color spaces to sRGB.
+ * If the source color space does not use CIE XYZ D65 as its profile
+ * connection space, the two spaces are chromatically adapted to use the
+ * CIE standard illuminant {@link #ILLUMINANT_D50 D50} as needed.
+ *
+ * If the source is the sRGB color space, an optimized connector
+ * is returned to avoid unnecessary computations and loss of precision.
+ *
+ * @param source The color space to convert colors from
+ * @param intent The render intent to map colors from the source to the destination
+ * @return A non-null connector between the specified color space and sRGB
+ *
+ * @see #connect(ColorSpace)
+ * @see #connect(ColorSpace, ColorSpace)
+ * @see #connect(ColorSpace, ColorSpace, RenderIntent)
+ */
+ @NonNull
+ public static Connector connect(@NonNull ColorSpace source, @NonNull RenderIntent intent) {
+ if (source.isSrgb()) return Connector.identity(source);
+ if (source.getModel() == Model.RGB) {
+ return new Connector.Rgb((Rgb) source, (Rgb) get(Named.SRGB), intent);
+ }
+ return new Connector(source, get(Named.SRGB), intent);
+ }
+ /**
+ * Performs the chromatic adaptation of a color space from its native
+ * white point to the specified white point.
+ *
+ * The chromatic adaptation is performed using the
+ * {@link Adaptation#BRADFORD} matrix.
+ *
+ * The color space returned by this method always has
+ * an ID of {@link #MIN_ID}.
+ *
+ * @param colorSpace The color space to chromatically adapt
+ * @param whitePoint The new white point
+ * @return A {@link ColorSpace} instance with the same name, primaries,
+ * transfer functions and range as the specified color space
+ *
+ * @see Adaptation
+ * @see #adapt(ColorSpace, float[], Adaptation)
+ */
+ @NonNull
+ public static ColorSpace adapt(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint) {
+ return adapt(colorSpace, whitePoint, Adaptation.BRADFORD);
+ }
+ /**
+ * Performs the chromatic adaptation of a color space from its native
+ * white point to the specified white point. If the specified color space
+ * does not have an {@link Model#RGB RGB} color model, or if the color
+ * space already has the target white point, the color space is returned
+ * unmodified.
+ *
+ * The chromatic adaptation is performed using the von Kries method
+ * described in the documentation of {@link Adaptation}.
+ *
+ * The color space returned by this method always has
+ * an ID of {@link #MIN_ID}.
+ *
+ * @param colorSpace The color space to chromatically adapt
+ * @param whitePoint The new white point
+ * @param adaptation The adaptation matrix
+ * @return A new color space if the specified color space has an RGB
+ * model and a white point different from the specified white
+ * point; the specified color space otherwise
+ *
+ * @see Adaptation
+ * @see #adapt(ColorSpace, float[])
+ */
+ @NonNull
+ public static ColorSpace adapt(@NonNull ColorSpace colorSpace,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull Adaptation adaptation) {
+ if (colorSpace.getModel() == Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
+ if (compare(rgb.mWhitePoint, whitePoint)) return colorSpace;
+ float[] xyz = whitePoint.length == 3 ?
+ Arrays.copyOf(whitePoint, 3) : xyYToXyz(whitePoint);
+ float[] adaptationTransform = chromaticAdaptation(adaptation.mTransform,
+ xyYToXyz(rgb.getWhitePoint()), xyz);
+ float[] transform = mul3x3(adaptationTransform, rgb.mTransform);
+ return new ColorSpace.Rgb(rgb, transform, whitePoint);
+ }
+ return colorSpace;
+ }
+ /**
+ * Helper method for creating native SkColorSpace.
+ *
+ * This essentially calls adapt on a ColorSpace that has not been fully
+ * created. It also does not fully create the adapted ColorSpace, but
+ * just returns the transform.
+ */
+ @NonNull @Size(9)
+ private static float[] adaptToIlluminantD50(
+ @NonNull @Size(2) float[] origWhitePoint,
+ @NonNull @Size(9) float[] origTransform) {
+ float[] desired = ILLUMINANT_D50;
+ if (compare(origWhitePoint, desired)) return origTransform;
+ float[] xyz = xyYToXyz(desired);
+ float[] adaptationTransform = chromaticAdaptation(Adaptation.BRADFORD.mTransform,
+ xyYToXyz(origWhitePoint), xyz);
+ return mul3x3(adaptationTransform, origTransform);
+ }
+ /**
+ * Returns an instance of {@link ColorSpace} whose ID matches the
+ * specified ID.
+ *
+ * This method always returns the same instance for a given ID.
+ *
+ * This method is thread-safe.
+ *
+ * @param index An integer ID between {@link #MIN_ID} and {@link #MAX_ID}
+ * @return A non-null {@link ColorSpace} instance
+ * @throws IllegalArgumentException If the ID does not match the ID of one of the
+ * {@link Named named color spaces}
+ */
+ @NonNull
+ static ColorSpace get(@IntRange(from = MIN_ID, to = MAX_ID) int index) {
+ if (index < 0 || index >= sNamedColorSpaces.length) {
+ throw new IllegalArgumentException("Invalid ID, must be in the range [0.." +
+ sNamedColorSpaces.length + ")");
+ }
+ return sNamedColorSpaces[index];
+ }
+ /**
+ * Create a {@link ColorSpace} object using a {@link android.hardware.DataSpace DataSpace}
+ * value.
+ *
+ * This function maps from a dataspace to a {@link Named} ColorSpace.
+ * If no {@link Named} ColorSpace object matching the {@code dataSpace} value can be created,
+ * {@code null} will return.
+ *
+ * @param dataSpace The dataspace value
+ * @return the ColorSpace object or {@code null} if no matching colorspace can be found.
+ */
+ @SuppressLint("MethodNameUnits")
+ @Nullable
+ public static ColorSpace getFromDataSpace(@NamedDataSpace int dataSpace) {
+ int index = sDataToColorSpaces.get(dataSpace, -1);
+ if (index != -1) {
+ return ColorSpace.get(index);
+ } else {
+ return null;
+ }
+ }
+ /**
+ * Retrieve the {@link android.hardware.DataSpace DataSpace} value from a {@link ColorSpace}
+ * object.
+ *
+ * If this {@link ColorSpace} object has no matching {@code dataSpace} value,
+ * {@link android.hardware.DataSpace#DATASPACE_UNKNOWN DATASPACE_UNKNOWN} will return.
+ *
+ * @return the dataspace value.
+ */
+ @SuppressLint("MethodNameUnits")
+ public @NamedDataSpace int getDataSpace() {
+ int index = sDataToColorSpaces.indexOfValue(getId());
+ if (index != -1) {
+ return sDataToColorSpaces.keyAt(index);
+ } else {
+ return DataSpace.DATASPACE_UNKNOWN;
+ }
+ }
+ /**
+ * Returns an instance of {@link ColorSpace} identified by the specified
+ * name. The list of names provided in the {@link Named} enum gives access
+ * to a variety of common RGB color spaces.
+ *
+ * This method always returns the same instance for a given name.
+ *
+ * This method is thread-safe.
+ *
+ * @param name The name of the color space to get an instance of
+ * @return A non-null {@link ColorSpace} instance
+ */
+ @NonNull
+ public static ColorSpace get(@NonNull Named name) {
+ return sNamedColorSpaces[name.ordinal()];
+ }
+ /**
+ * Returns a {@link Named} instance of {@link ColorSpace} that matches
+ * the specified RGB to CIE XYZ transform and transfer functions. If no
+ * instance can be found, this method returns null.
+ *
+ * The color transform matrix is assumed to target the CIE XYZ space
+ * a {@link #ILLUMINANT_D50 D50} standard illuminant.
+ *
+ * @param toXYZD50 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param function Parameters for the transfer functions
+ * @return A non-null {@link ColorSpace} if a match is found, null otherwise
+ */
+ @Nullable
+ public static ColorSpace match(
+ @NonNull @Size(9) float[] toXYZD50,
+ @NonNull Rgb.TransferParameters function) {
+ for (ColorSpace colorSpace : sNamedColorSpaces) {
+ if (colorSpace.getModel() == Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) adapt(colorSpace, ILLUMINANT_D50_XYZ);
+ if (compare(toXYZD50, rgb.mTransform) &&
+ compare(function, rgb.mTransferParameters)) {
+ return colorSpace;
+ }
+ }
+ }
+ return null;
+ }
+ static {
+ sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb(
+ "sRGB IEC61966-2.1",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ SRGB_TRANSFER_PARAMETERS,
+ Named.SRGB.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB, Named.SRGB.ordinal());
+ sNamedColorSpaces[Named.LINEAR_SRGB.ordinal()] = new ColorSpace.Rgb(
+ "sRGB IEC61966-2.1 (Linear)",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ 1.0,
+ 0.0f, 1.0f,
+ Named.LINEAR_SRGB.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_SRGB_LINEAR, Named.LINEAR_SRGB.ordinal());
+ sNamedColorSpaces[Named.EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
+ "scRGB-nl IEC 61966-2-2:2003",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ x -> absRcpResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ x -> absResponse(x, 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4),
+ -0.799f, 2.399f,
+ SRGB_TRANSFER_PARAMETERS,
+ Named.EXTENDED_SRGB.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_SCRGB, Named.EXTENDED_SRGB.ordinal());
+ sNamedColorSpaces[Named.LINEAR_EXTENDED_SRGB.ordinal()] = new ColorSpace.Rgb(
+ "scRGB IEC 61966-2-2:2003",
+ SRGB_PRIMARIES,
+ ILLUMINANT_D65,
+ 1.0,
+ -0.5f, 7.499f,
+ Named.LINEAR_EXTENDED_SRGB.ordinal()
+ );
+ sDataToColorSpaces.put(
+ DataSpace.DATASPACE_SCRGB_LINEAR, Named.LINEAR_EXTENDED_SRGB.ordinal());
+ sNamedColorSpaces[Named.BT709.ordinal()] = new ColorSpace.Rgb(
+ "Rec. ITU-R BT.709-5",
+ new float[] { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f },
+ ILLUMINANT_D65,
+ null,
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
+ Named.BT709.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal());
+ sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb(
+ "Rec. ITU-R BT.2020-1",
+ new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f },
+ ILLUMINANT_D65,
+ null,
+ new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45),
+ Named.BT2020.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020, Named.BT2020.ordinal());
+ sNamedColorSpaces[Named.DCI_P3.ordinal()] = new ColorSpace.Rgb(
+ "SMPTE RP 431-2-2007 DCI (P3)",
+ new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f },
+ new float[] { 0.314f, 0.351f },
+ 2.6,
+ 0.0f, 1.0f,
+ Named.DCI_P3.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_DCI_P3, Named.DCI_P3.ordinal());
+ sNamedColorSpaces[Named.DISPLAY_P3.ordinal()] = new ColorSpace.Rgb(
+ "Display P3",
+ new float[] { 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f },
+ ILLUMINANT_D65,
+ null,
+ SRGB_TRANSFER_PARAMETERS,
+ Named.DISPLAY_P3.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_DISPLAY_P3, Named.DISPLAY_P3.ordinal());
+ sNamedColorSpaces[Named.NTSC_1953.ordinal()] = new ColorSpace.Rgb(
+ "NTSC (1953)",
+ NTSC_1953_PRIMARIES,
+ ILLUMINANT_C,
+ null,
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
+ Named.NTSC_1953.ordinal()
+ );
+ sNamedColorSpaces[Named.SMPTE_C.ordinal()] = new ColorSpace.Rgb(
+ "SMPTE-C RGB",
+ new float[] { 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f },
+ ILLUMINANT_D65,
+ null,
+ new Rgb.TransferParameters(1 / 1.099, 0.099 / 1.099, 1 / 4.5, 0.081, 1 / 0.45),
+ Named.SMPTE_C.ordinal()
+ );
+ sNamedColorSpaces[Named.ADOBE_RGB.ordinal()] = new ColorSpace.Rgb(
+ "Adobe RGB (1998)",
+ new float[] { 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f },
+ ILLUMINANT_D65,
+ 2.2,
+ 0.0f, 1.0f,
+ Named.ADOBE_RGB.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_ADOBE_RGB, Named.ADOBE_RGB.ordinal());
+ sNamedColorSpaces[Named.PRO_PHOTO_RGB.ordinal()] = new ColorSpace.Rgb(
+ "ROMM RGB ISO 22028-2:2013",
+ new float[] { 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f },
+ ILLUMINANT_D50,
+ null,
+ new Rgb.TransferParameters(1.0, 0.0, 1 / 16.0, 0.031248, 1.8),
+ Named.PRO_PHOTO_RGB.ordinal()
+ );
+ sNamedColorSpaces[Named.ACES.ordinal()] = new ColorSpace.Rgb(
+ "SMPTE ST 2065-1:2012 ACES",
+ new float[] { 0.73470f, 0.26530f, 0.0f, 1.0f, 0.00010f, -0.0770f },
+ ILLUMINANT_D60,
+ 1.0,
+ -65504.0f, 65504.0f,
+ Named.ACES.ordinal()
+ );
+ sNamedColorSpaces[Named.ACESCG.ordinal()] = new ColorSpace.Rgb(
+ "Academy S-2014-004 ACEScg",
+ new float[] { 0.713f, 0.293f, 0.165f, 0.830f, 0.128f, 0.044f },
+ ILLUMINANT_D60,
+ 1.0,
+ -65504.0f, 65504.0f,
+ Named.ACESCG.ordinal()
+ );
+ sNamedColorSpaces[Named.CIE_XYZ.ordinal()] = new Xyz(
+ "Generic XYZ",
+ Named.CIE_XYZ.ordinal()
+ );
+ sNamedColorSpaces[Named.CIE_LAB.ordinal()] = new ColorSpace.Lab(
+ "Generic L*a*b*",
+ Named.CIE_LAB.ordinal()
+ );
+ }
+ // Reciprocal piecewise gamma response
+ private static double rcpResponse(double x, double a, double b, double c, double d, double g) {
+ return x >= d * c ? (Math.pow(x, 1.0 / g) - b) / a : x / c;
+ }
+ // Piecewise gamma response
+ private static double response(double x, double a, double b, double c, double d, double g) {
+ return x >= d ? Math.pow(a * x + b, g) : c * x;
+ }
+ // Reciprocal piecewise gamma response
+ private static double rcpResponse(double x, double a, double b, double c, double d,
+ double e, double f, double g) {
+ return x >= d * c ? (Math.pow(x - e, 1.0 / g) - b) / a : (x - f) / c;
+ }
+ // Piecewise gamma response
+ private static double response(double x, double a, double b, double c, double d,
+ double e, double f, double g) {
+ return x >= d ? Math.pow(a * x + b, g) + e : c * x + f;
+ }
+ // Reciprocal piecewise gamma response, encoded as sign(x).f(abs(x)) for color
+ // spaces that allow negative values
+ @SuppressWarnings("SameParameterValue")
+ private static double absRcpResponse(double x, double a, double b, double c, double d, double g) {
+ return Math.copySign(rcpResponse(x < 0.0 ? -x : x, a, b, c, d, g), x);
+ }
+ // Piecewise gamma response, encoded as sign(x).f(abs(x)) for color spaces that
+ // allow negative values
+ @SuppressWarnings("SameParameterValue")
+ private static double absResponse(double x, double a, double b, double c, double d, double g) {
+ return Math.copySign(response(x < 0.0 ? -x : x, a, b, c, d, g), x);
+ }
+ /**
+ * Compares two sets of parametric transfer functions parameters with a precision of 1e-3.
+ *
+ * @param a The first set of parameters to compare
+ * @param b The second set of parameters to compare
+ * @return True if the two sets are equal, false otherwise
+ */
+ private static boolean compare(
+ @Nullable Rgb.TransferParameters a,
+ @Nullable Rgb.TransferParameters b) {
+ //noinspection SimplifiableIfStatement
+ if (a == null && b == null) return true;
+ return a != null && b != null &&
+ Math.abs(a.a - b.a) < 1e-3 &&
+ Math.abs(a.b - b.b) < 1e-3 &&
+ Math.abs(a.c - b.c) < 1e-3 &&
+ Math.abs(a.d - b.d) < 2e-3 && // Special case for variations in sRGB OETF/EOTF
+ Math.abs(a.e - b.e) < 1e-3 &&
+ Math.abs(a.f - b.f) < 1e-3 &&
+ Math.abs(a.g - b.g) < 1e-3;
+ }
+ /**
+ * Compares two arrays of float with a precision of 1e-3.
+ *
+ * @param a The first array to compare
+ * @param b The second array to compare
+ * @return True if the two arrays are equal, false otherwise
+ */
+ private static boolean compare(@NonNull float[] a, @NonNull float[] b) {
+ if (a == b) return true;
+ for (int i = 0; i < a.length; i++) {
+ if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > 1e-3f) return false;
+ }
+ return true;
+ }
+ /**
+ * Inverts a 3x3 matrix. This method assumes the matrix is invertible.
+ *
+ * @param m A 3x3 matrix as a non-null array of 9 floats
+ * @return A new array of 9 floats containing the inverse of the input matrix
+ */
+ @NonNull
+ @Size(9)
+ private static float[] inverse3x3(@NonNull @Size(9) float[] m) {
+ float a = m[0];
+ float b = m[3];
+ float c = m[6];
+ float d = m[1];
+ float e = m[4];
+ float f = m[7];
+ float g = m[2];
+ float h = m[5];
+ float i = m[8];
+ float A = e * i - f * h;
+ float B = f * g - d * i;
+ float C = d * h - e * g;
+ float det = a * A + b * B + c * C;
+ float inverted[] = new float[m.length];
+ inverted[0] = A / det;
+ inverted[1] = B / det;
+ inverted[2] = C / det;
+ inverted[3] = (c * h - b * i) / det;
+ inverted[4] = (a * i - c * g) / det;
+ inverted[5] = (b * g - a * h) / det;
+ inverted[6] = (b * f - c * e) / det;
+ inverted[7] = (c * d - a * f) / det;
+ inverted[8] = (a * e - b * d) / det;
+ return inverted;
+ }
+ /**
+ * Multiplies two 3x3 matrices, represented as non-null arrays of 9 floats.
+ *
+ * @param lhs 3x3 matrix, as a non-null array of 9 floats
+ * @param rhs 3x3 matrix, as a non-null array of 9 floats
+ * @return A new array of 9 floats containing the result of the multiplication
+ * of rhs by lhs
+ */
+ @NonNull
+ @Size(9)
+ private static float[] mul3x3(@NonNull @Size(9) float[] lhs, @NonNull @Size(9) float[] rhs) {
+ float[] r = new float[9];
+ r[0] = lhs[0] * rhs[0] + lhs[3] * rhs[1] + lhs[6] * rhs[2];
+ r[1] = lhs[1] * rhs[0] + lhs[4] * rhs[1] + lhs[7] * rhs[2];
+ r[2] = lhs[2] * rhs[0] + lhs[5] * rhs[1] + lhs[8] * rhs[2];
+ r[3] = lhs[0] * rhs[3] + lhs[3] * rhs[4] + lhs[6] * rhs[5];
+ r[4] = lhs[1] * rhs[3] + lhs[4] * rhs[4] + lhs[7] * rhs[5];
+ r[5] = lhs[2] * rhs[3] + lhs[5] * rhs[4] + lhs[8] * rhs[5];
+ r[6] = lhs[0] * rhs[6] + lhs[3] * rhs[7] + lhs[6] * rhs[8];
+ r[7] = lhs[1] * rhs[6] + lhs[4] * rhs[7] + lhs[7] * rhs[8];
+ r[8] = lhs[2] * rhs[6] + lhs[5] * rhs[7] + lhs[8] * rhs[8];
+ return r;
+ }
+ /**
+ * Multiplies a vector of 3 components by a 3x3 matrix and stores the
+ * result in the input vector.
+ *
+ * @param lhs 3x3 matrix, as a non-null array of 9 floats
+ * @param rhs Vector of 3 components, as a non-null array of 3 floats
+ * @return The array of 3 passed as the rhs parameter
+ */
+ @NonNull
+ @Size(min = 3)
+ private static float[] mul3x3Float3(
+ @NonNull @Size(9) float[] lhs, @NonNull @Size(min = 3) float[] rhs) {
+ float r0 = rhs[0];
+ float r1 = rhs[1];
+ float r2 = rhs[2];
+ rhs[0] = lhs[0] * r0 + lhs[3] * r1 + lhs[6] * r2;
+ rhs[1] = lhs[1] * r0 + lhs[4] * r1 + lhs[7] * r2;
+ rhs[2] = lhs[2] * r0 + lhs[5] * r1 + lhs[8] * r2;
+ return rhs;
+ }
+ /**
+ * Multiplies a diagonal 3x3 matrix lhs, represented as an array of 3 floats,
+ * by a 3x3 matrix represented as an array of 9 floats.
+ *
+ * @param lhs Diagonal 3x3 matrix, as a non-null array of 3 floats
+ * @param rhs 3x3 matrix, as a non-null array of 9 floats
+ * @return A new array of 9 floats containing the result of the multiplication
+ * of rhs by lhs
+ */
+ @NonNull
+ @Size(9)
+ private static float[] mul3x3Diag(
+ @NonNull @Size(3) float[] lhs, @NonNull @Size(9) float[] rhs) {
+ return new float[] {
+ lhs[0] * rhs[0], lhs[1] * rhs[1], lhs[2] * rhs[2],
+ lhs[0] * rhs[3], lhs[1] * rhs[4], lhs[2] * rhs[5],
+ lhs[0] * rhs[6], lhs[1] * rhs[7], lhs[2] * rhs[8]
+ };
+ }
+ /**
+ * Converts a value from CIE xyY to CIE XYZ. Y is assumed to be 1 so the
+ * input xyY array only contains the x and y components.
+ *
+ * @param xyY The xyY value to convert to XYZ, cannot be null, length must be 2
+ * @return A new float array of length 3 containing XYZ values
+ */
+ @NonNull
+ @Size(3)
+ private static float[] xyYToXyz(@NonNull @Size(2) float[] xyY) {
+ return new float[] { xyY[0] / xyY[1], 1.0f, (1 - xyY[0] - xyY[1]) / xyY[1] };
+ }
+ /**
+ * Computes the chromatic adaptation transform from the specified
+ * source white point to the specified destination white point.
+ *
+ * The transform is computed using the von Kries method, described
+ * in more details in the documentation of {@link Adaptation}. The
+ * {@link Adaptation} enum provides different matrices that can be
+ * used to perform the adaptation.
+ *
+ * @param matrix The adaptation matrix
+ * @param srcWhitePoint The white point to adapt from, *will be modified*
+ * @param dstWhitePoint The white point to adapt to, *will be modified*
+ * @return A 3x3 matrix as a non-null array of 9 floats
+ */
+ @NonNull
+ @Size(9)
+ private static float[] chromaticAdaptation(@NonNull @Size(9) float[] matrix,
+ @NonNull @Size(3) float[] srcWhitePoint, @NonNull @Size(3) float[] dstWhitePoint) {
+ float[] srcLMS = mul3x3Float3(matrix, srcWhitePoint);
+ float[] dstLMS = mul3x3Float3(matrix, dstWhitePoint);
+ // LMS is a diagonal matrix stored as a float[3]
+ float[] LMS = { dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2] };
+ return mul3x3(inverse3x3(matrix), mul3x3Diag(LMS, matrix));
+ }
+ /**
+ * Computes the chromaticity coordinates of a specified correlated color
+ * temperature (CCT) on the Planckian locus. The specified CCT must be
+ * greater than 0. A meaningful CCT range is [1667, 25000].
+ *
+ * The transform is computed using the methods in Kang et
+ * al., Design of Advanced Color - Temperature Control System for HDTV
+ * Applications, Journal of Korean Physical Society 41, 865-871
+ * (2002).
+ *
+ * @param cct The correlated color temperature, in Kelvin
+ * @return Corresponding XYZ values
+ * @throws IllegalArgumentException If cct is invalid
+ */
+ @NonNull
+ @Size(3)
+ public static float[] cctToXyz(@IntRange(from = 1) int cct) {
+ if (cct < 1) {
+ throw new IllegalArgumentException("Temperature must be greater than 0");
+ }
+ final float icct = 1e3f / cct;
+ final float icct2 = icct * icct;
+ final float x = cct <= 4000.0f ?
+ 0.179910f + 0.8776956f * icct - 0.2343589f * icct2 - 0.2661239f * icct2 * icct :
+ 0.240390f + 0.2226347f * icct + 2.1070379f * icct2 - 3.0258469f * icct2 * icct;
+ final float x2 = x * x;
+ final float y = cct <= 2222.0f ?
+ -0.20219683f + 2.18555832f * x - 1.34811020f * x2 - 1.1063814f * x2 * x :
+ cct <= 4000.0f ?
+ -0.16748867f + 2.09137015f * x - 1.37418593f * x2 - 0.9549476f * x2 * x :
+ -0.37001483f + 3.75112997f * x - 5.8733867f * x2 + 3.0817580f * x2 * x;
+ return xyYToXyz(new float[] {x, y});
+ }
+ /**
+ * Computes the chromatic adaptation transform from the specified
+ * source white point to the specified destination white point.
+ *
+ * The transform is computed using the von Kries method, described
+ * in more details in the documentation of {@link Adaptation}. The
+ * {@link Adaptation} enum provides different matrices that can be
+ * used to perform the adaptation.
+ *
+ * @param adaptation The adaptation method
+ * @param srcWhitePoint The white point to adapt from
+ * @param dstWhitePoint The white point to adapt to
+ * @return A 3x3 matrix as a non-null array of 9 floats
+ */
+ @NonNull
+ @Size(9)
+ public static float[] chromaticAdaptation(@NonNull Adaptation adaptation,
+ @NonNull @Size(min = 2, max = 3) float[] srcWhitePoint,
+ @NonNull @Size(min = 2, max = 3) float[] dstWhitePoint) {
+ if ((srcWhitePoint.length != 2 && srcWhitePoint.length != 3)
+ || (dstWhitePoint.length != 2 && dstWhitePoint.length != 3)) {
+ throw new IllegalArgumentException("A white point array must have 2 or 3 floats");
+ }
+ float[] srcXyz = srcWhitePoint.length == 3 ?
+ Arrays.copyOf(srcWhitePoint, 3) : xyYToXyz(srcWhitePoint);
+ float[] dstXyz = dstWhitePoint.length == 3 ?
+ Arrays.copyOf(dstWhitePoint, 3) : xyYToXyz(dstWhitePoint);
+ if (compare(srcXyz, dstXyz)) {
+ return new float[] {
+ 1.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f
+ };
+ }
+ return chromaticAdaptation(adaptation.mTransform, srcXyz, dstXyz);
+ }
+ /**
+ * Implementation of the CIE XYZ color space. Assumes the white point is D50.
+ */
+ @AnyThread
+ private static final class Xyz extends ColorSpace {
+ private Xyz(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.XYZ, id);
+ }
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return -2.0f;
+ }
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return 2.0f;
+ }
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0]);
+ v[1] = clamp(v[1]);
+ v[2] = clamp(v[2]);
+ return v;
+ }
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0]);
+ v[1] = clamp(v[1]);
+ v[2] = clamp(v[2]);
+ return v;
+ }
+ private static float clamp(float x) {
+ return x < -2.0f ? -2.0f : x > 2.0f ? 2.0f : x;
+ }
+ }
+ /**
+ * Implementation of the CIE L*a*b* color space. Its PCS is CIE XYZ
+ * with a white point of D50.
+ */
+ @AnyThread
+ private static final class Lab extends ColorSpace {
+ private static final float A = 216.0f / 24389.0f;
+ private static final float B = 841.0f / 108.0f;
+ private static final float C = 4.0f / 29.0f;
+ private static final float D = 6.0f / 29.0f;
+ private Lab(@NonNull String name, @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.LAB, id);
+ }
+ @Override
+ public boolean isWideGamut() {
+ return true;
+ }
+ @Override
+ public float getMinValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 0.0f : -128.0f;
+ }
+ @Override
+ public float getMaxValue(@IntRange(from = 0, to = 3) int component) {
+ return component == 0 ? 100.0f : 128.0f;
+ }
+ @Override
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = clamp(v[0], 0.0f, 100.0f);
+ v[1] = clamp(v[1], -128.0f, 128.0f);
+ v[2] = clamp(v[2], -128.0f, 128.0f);
+ float fy = (v[0] + 16.0f) / 116.0f;
+ float fx = fy + (v[1] * 0.002f);
+ float fz = fy - (v[2] * 0.005f);
+ float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C);
+ float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C);
+ float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C);
+ v[0] = X * ILLUMINANT_D50_XYZ[0];
+ v[1] = Y * ILLUMINANT_D50_XYZ[1];
+ v[2] = Z * ILLUMINANT_D50_XYZ[2];
+ return v;
+ }
+ @Override
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ float X = v[0] / ILLUMINANT_D50_XYZ[0];
+ float Y = v[1] / ILLUMINANT_D50_XYZ[1];
+ float Z = v[2] / ILLUMINANT_D50_XYZ[2];
+ float fx = X > A ? (float) Math.pow(X, 1.0 / 3.0) : B * X + C;
+ float fy = Y > A ? (float) Math.pow(Y, 1.0 / 3.0) : B * Y + C;
+ float fz = Z > A ? (float) Math.pow(Z, 1.0 / 3.0) : B * Z + C;
+ float L = 116.0f * fy - 16.0f;
+ float a = 500.0f * (fx - fy);
+ float b = 200.0f * (fy - fz);
+ v[0] = clamp(L, 0.0f, 100.0f);
+ v[1] = clamp(a, -128.0f, 128.0f);
+ v[2] = clamp(b, -128.0f, 128.0f);
+ return v;
+ }
+ private static float clamp(float x, float min, float max) {
+ return x < min ? min : x > max ? max : x;
+ }
+ }
+ /**
+ * Retrieve the native SkColorSpace object for passing to native.
+ *
+ * Only valid on ColorSpace.Rgb.
+ */
+ long getNativeInstance() {
+ throw new IllegalArgumentException("colorSpace must be an RGB color space");
+ }
+ /**
+ * {@usesMathJax}
+ *
+ * An RGB color space is an additive color space using the
+ * {@link Model#RGB RGB} color model (a color is therefore represented
+ * by a tuple of 3 numbers).
+ *
+ * A specific RGB color space is defined by the following properties:
+ *
+ * - Three chromaticities of the red, green and blue primaries, which
+ * define the gamut of the color space.
+ * - A white point chromaticity that defines the stimulus to which
+ * color space values are normalized (also just called "white").
+ * - An opto-electronic transfer function, also called opto-electronic
+ * conversion function or often, and approximately, gamma function.
+ * - An electro-optical transfer function, also called electo-optical
+ * conversion function or often, and approximately, gamma function.
+ * - A range of valid RGB values (most commonly \([0..1]\)).
+ *
+ *
+ * The most commonly used RGB color space is {@link Named#SRGB sRGB}.
+ *
+ * Primaries and white point chromaticities
+ * In this implementation, the chromaticity of the primaries and the white
+ * point of an RGB color space is defined in the CIE xyY color space. This
+ * color space separates the chromaticity of a color, the x and y components,
+ * and its luminance, the Y component. Since the primaries and the white
+ * point have full brightness, the Y component is assumed to be 1 and only
+ * the x and y components are needed to encode them.
+ * For convenience, this implementation also allows to define the
+ * primaries and white point in the CIE XYZ space. The tristimulus XYZ values
+ * are internally converted to xyY.
+ *
+ *
+ *
+ * sRGB primaries and white point
+ *
+ *
+ * Transfer functions
+ * A transfer function is a color component conversion function, defined as
+ * a single variable, monotonic mathematical function. It is applied to each
+ * individual component of a color. They are used to perform the mapping
+ * between linear tristimulus values and non-linear electronic signal value.
+ * The opto-electronic transfer function (OETF or OECF) encodes
+ * tristimulus values in a scene to a non-linear electronic signal value.
+ * An OETF is often expressed as a power function with an exponent between
+ * 0.38 and 0.55 (the reciprocal of 1.8 to 2.6).
+ * The electro-optical transfer function (EOTF or EOCF) decodes
+ * a non-linear electronic signal value to a tristimulus value at the display.
+ * An EOTF is often expressed as a power function with an exponent between
+ * 1.8 and 2.6.
+ * Transfer functions are used as a compression scheme. For instance,
+ * linear sRGB values would normally require 11 to 12 bits of precision to
+ * store all values that can be perceived by the human eye. When encoding
+ * sRGB values using the appropriate OETF (see {@link Named#SRGB sRGB} for
+ * an exact mathematical description of that OETF), the values can be
+ * compressed to only 8 bits precision.
+ * When manipulating RGB values, particularly sRGB values, it is safe
+ * to assume that these values have been encoded with the appropriate
+ * OETF (unless noted otherwise). Encoded values are often said to be in
+ * "gamma space". They are therefore defined in a non-linear space. This
+ * in turns means that any linear operation applied to these values is
+ * going to yield mathematically incorrect results (any linear interpolation
+ * such as gradient generation for instance, most image processing functions
+ * such as blurs, etc.).
+ * To properly process encoded RGB values you must first apply the
+ * EOTF to decode the value into linear space. After processing, the RGB
+ * value must be encoded back to non-linear ("gamma") space. Here is a
+ * formal description of the process, where \(f\) is the processing
+ * function to apply:
+ *
+ * $$RGB_{out} = OETF(f(EOTF(RGB_{in})))$$
+ *
+ * If the transfer functions of the color space can be expressed as an
+ * ICC parametric curve as defined in ICC.1:2004-10, the numeric parameters
+ * can be retrieved by calling {@link #getTransferParameters()}. This can
+ * be useful to match color spaces for instance.
+ *
+ * Some RGB color spaces, such as {@link Named#ACES} and
+ * {@link Named#LINEAR_EXTENDED_SRGB scRGB}, are said to be linear because
+ * their transfer functions are the identity function: \(f(x) = x\).
+ * If the source and/or destination are known to be linear, it is not
+ * necessary to invoke the transfer functions.
+ *
+ * Range
+ * Most RGB color spaces allow RGB values in the range \([0..1]\). There
+ * are however a few RGB color spaces that allow much larger ranges. For
+ * instance, {@link Named#EXTENDED_SRGB scRGB} is used to manipulate the
+ * range \([-0.5..7.5]\) while {@link Named#ACES ACES} can be used throughout
+ * the range \([-65504, 65504]\).
+ *
+ *
+ *
+ * Extended sRGB and its large range
+ *
+ *
+ * Converting between RGB color spaces
+ * Conversion between two color spaces is achieved by using an intermediate
+ * color space called the profile connection space (PCS). The PCS used by
+ * this implementation is CIE XYZ. The conversion operation is defined
+ * as such:
+ *
+ * $$RGB_{out} = OETF(T_{dst}^{-1} \cdot T_{src} \cdot EOTF(RGB_{in}))$$
+ *
+ * Where \(T_{src}\) is the {@link #getTransform() RGB to XYZ transform}
+ * of the source color space and \(T_{dst}^{-1}\) the {@link #getInverseTransform()
+ * XYZ to RGB transform} of the destination color space.
+ * Many RGB color spaces commonly used with electronic devices use the
+ * standard illuminant {@link #ILLUMINANT_D65 D65}. Care must be take however
+ * when converting between two RGB color spaces if their white points do not
+ * match. This can be achieved by either calling
+ * {@link #adapt(ColorSpace, float[])} to adapt one or both color spaces to
+ * a single common white point. This can be achieved automatically by calling
+ * {@link ColorSpace#connect(ColorSpace, ColorSpace)}, which also handles
+ * non-RGB color spaces.
+ * To learn more about the white point adaptation process, refer to the
+ * documentation of {@link Adaptation}.
+ */
+ @AnyThread
+ public static class Rgb extends ColorSpace {
+ /**
+ * {@usesMathJax}
+ *
+ * Defines the parameters for the ICC parametric curve type 4, as
+ * defined in ICC.1:2004-10, section 10.15.
+ *
+ * The EOTF is of the form:
+ *
+ * \(\begin{equation}
+ * Y = \begin{cases}c X + f & X \lt d \\\
+ * \left( a X + b \right) ^{g} + e & X \ge d \end{cases}
+ * \end{equation}\)
+ *
+ * The corresponding OETF is simply the inverse function.
+ *
+ * The parameters defined by this class form a valid transfer
+ * function only if all the following conditions are met:
+ *
+ * - No parameter is a {@link Double#isNaN(double) Not-a-Number}
+ * - \(d\) is in the range \([0..1]\)
+ * - The function is not constant
+ * - The function is positive and increasing
+ *
+ */
+ public static class TransferParameters {
+ /** Variable \(a\) in the equation of the EOTF described above. */
+ public final double a;
+ /** Variable \(b\) in the equation of the EOTF described above. */
+ public final double b;
+ /** Variable \(c\) in the equation of the EOTF described above. */
+ public final double c;
+ /** Variable \(d\) in the equation of the EOTF described above. */
+ public final double d;
+ /** Variable \(e\) in the equation of the EOTF described above. */
+ public final double e;
+ /** Variable \(f\) in the equation of the EOTF described above. */
+ public final double f;
+ /** Variable \(g\) in the equation of the EOTF described above. */
+ public final double g;
+ /**
+ * Defines the parameters for the ICC parametric curve type 3, as
+ * defined in ICC.1:2004-10, section 10.15.
+ *
+ * The EOTF is of the form:
+ *
+ * \(\begin{equation}
+ * Y = \begin{cases}c X & X \lt d \\\
+ * \left( a X + b \right) ^{g} & X \ge d \end{cases}
+ * \end{equation}\)
+ *
+ * This constructor is equivalent to setting \(e\) and \(f\) to 0.
+ *
+ * @param a The value of \(a\) in the equation of the EOTF described above
+ * @param b The value of \(b\) in the equation of the EOTF described above
+ * @param c The value of \(c\) in the equation of the EOTF described above
+ * @param d The value of \(d\) in the equation of the EOTF described above
+ * @param g The value of \(g\) in the equation of the EOTF described above
+ *
+ * @throws IllegalArgumentException If the parameters form an invalid transfer function
+ */
+ public TransferParameters(double a, double b, double c, double d, double g) {
+ this(a, b, c, d, 0.0, 0.0, g);
+ }
+ /**
+ * Defines the parameters for the ICC parametric curve type 4, as
+ * defined in ICC.1:2004-10, section 10.15.
+ *
+ * @param a The value of \(a\) in the equation of the EOTF described above
+ * @param b The value of \(b\) in the equation of the EOTF described above
+ * @param c The value of \(c\) in the equation of the EOTF described above
+ * @param d The value of \(d\) in the equation of the EOTF described above
+ * @param e The value of \(e\) in the equation of the EOTF described above
+ * @param f The value of \(f\) in the equation of the EOTF described above
+ * @param g The value of \(g\) in the equation of the EOTF described above
+ *
+ * @throws IllegalArgumentException If the parameters form an invalid transfer function
+ */
+ public TransferParameters(double a, double b, double c, double d, double e,
+ double f, double g) {
+ if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) ||
+ Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) ||
+ Double.isNaN(g)) {
+ throw new IllegalArgumentException("Parameters cannot be NaN");
+ }
+ // Next representable float after 1.0
+ // We use doubles here but the representation inside our native code is often floats
+ if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) {
+ throw new IllegalArgumentException("Parameter d must be in the range [0..1], " +
+ "was " + d);
+ }
+ if (d == 0.0 && (a == 0.0 || g == 0.0)) {
+ throw new IllegalArgumentException(
+ "Parameter a or g is zero, the transfer function is constant");
+ }
+ if (d >= 1.0 && c == 0.0) {
+ throw new IllegalArgumentException(
+ "Parameter c is zero, the transfer function is constant");
+ }
+ if ((a == 0.0 || g == 0.0) && c == 0.0) {
+ throw new IllegalArgumentException("Parameter a or g is zero," +
+ " and c is zero, the transfer function is constant");
+ }
+ if (c < 0.0) {
+ throw new IllegalArgumentException("The transfer function must be increasing");
+ }
+ if (a < 0.0 || g < 0.0) {
+ throw new IllegalArgumentException("The transfer function must be " +
+ "positive or increasing");
+ }
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ this.e = e;
+ this.f = f;
+ this.g = g;
+ }
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TransferParameters that = (TransferParameters) o;
+ if (Double.compare(that.a, a) != 0) return false;
+ if (Double.compare(that.b, b) != 0) return false;
+ if (Double.compare(that.c, c) != 0) return false;
+ if (Double.compare(that.d, d) != 0) return false;
+ if (Double.compare(that.e, e) != 0) return false;
+ if (Double.compare(that.f, f) != 0) return false;
+ return Double.compare(that.g, g) == 0;
+ }
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(a);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(b);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(c);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(d);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(e);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(f);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(g);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+ }
+ @NonNull private final float[] mWhitePoint;
+ @NonNull private final float[] mPrimaries;
+ @NonNull private final float[] mTransform;
+ @NonNull private final float[] mInverseTransform;
+ @NonNull private final DoubleUnaryOperator mOetf;
+ @NonNull private final DoubleUnaryOperator mEotf;
+ @NonNull private final DoubleUnaryOperator mClampedOetf;
+ @NonNull private final DoubleUnaryOperator mClampedEotf;
+ private final float mMin;
+ private final float mMax;
+ private final boolean mIsWideGamut;
+ private final boolean mIsSrgb;
+ @Nullable private final TransferParameters mTransferParameters;
+ private final long mNativePtr;
+ @Override
+ long getNativeInstance() {
+ if (mNativePtr == 0) {
+ // If this object has TransferParameters, it must have a native object.
+ throw new IllegalArgumentException("ColorSpace must use an ICC "
+ + "parametric transfer function! used " + this);
+ }
+ return mNativePtr;
+ }
+ private static native long nativeGetNativeFinalizer();
+ private static native long nativeCreate(float a, float b, float c, float d,
+ float e, float f, float g, float[] xyz);
+ /**
+ * Creates a new RGB color space using a 3x3 column-major transform matrix.
+ * The transform matrix must convert from the RGB space to the profile connection
+ * space CIE XYZ.
+ *
+ * The range of the color space is imposed to be \([0..1]\).
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param toXYZ 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param oetf Opto-electronic transfer function, cannot be null
+ * @param eotf Electro-optical transfer function, cannot be null
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The OETF is null or the EOTF is null.
+ * - The minimum valid value is >= the maximum valid value.
+ *
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), null,
+ oetf, eotf, 0.0f, 1.0f, null, MIN_ID);
+ }
+ /**
+ * Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.
+ *
+ * The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ *
+ *
+ * Space | Primaries length | White point length |
+ * xyY | 6 | 2 |
+ * XYZ | 9 | 3 |
+ *
+ *
+ * When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.
+ *
+ * The ID, as returned by {@link #getId()}, of an object created by
+ * this constructor is always {@link #MIN_ID}.
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param oetf Opto-electronic transfer function, cannot be null
+ * @param eotf Electro-optical transfer function, cannot be null
+ * @param min The minimum valid value in this color space's RGB range
+ * @param max The maximum valid value in this color space's RGB range
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The primaries array is null or has a length that is neither 6 or 9.
+ * - The white point array is null or has a length that is neither 2 or 3.
+ * - The OETF is null or the EOTF is null.
+ * - The minimum valid value is >= the maximum valid value.
+ *
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf,
+ float min,
+ float max) {
+ this(name, primaries, whitePoint, null, oetf, eotf, min, max, null, MIN_ID);
+ }
+ /**
+ * Creates a new RGB color space using a 3x3 column-major transform matrix.
+ * The transform matrix must convert from the RGB space to the profile connection
+ * space CIE XYZ.
+ *
+ * The range of the color space is imposed to be \([0..1]\).
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param toXYZ 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param function Parameters for the transfer functions
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - Gamma is negative.
+ *
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ @NonNull TransferParameters function) {
+ // Note: when isGray() returns false, this passes null for the transform for
+ // consistency with other constructors, which compute the transform from the primaries
+ // and white point.
+ this(name, isGray(toXYZ) ? GRAY_PRIMARIES : computePrimaries(toXYZ),
+ computeWhitePoint(toXYZ), isGray(toXYZ) ? toXYZ : null, function, MIN_ID);
+ }
+ /**
+ * Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.
+ *
+ * The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ *
+ *
+ * Space | Primaries length | White point length |
+ * xyY | 6 | 2 |
+ * XYZ | 9 | 3 |
+ *
+ *
+ * When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param function Parameters for the transfer functions
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The primaries array is null or has a length that is neither 6 or 9.
+ * - The white point array is null or has a length that is neither 2 or 3.
+ * - The transfer parameters are invalid.
+ *
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @NonNull TransferParameters function) {
+ this(name, primaries, whitePoint, null, function, MIN_ID);
+ }
+ /**
+ * Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.
+ *
+ * The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ *
+ *
+ * Space | Primaries length | White point length |
+ * xyY | 6 | 2 |
+ * XYZ | 9 | 3 |
+ *
+ *
+ * When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param transform Computed transform matrix that converts from RGB to XYZ, or
+ * {@code null} to compute it from {@code primaries} and {@code whitePoint}.
+ * @param function Parameters for the transfer functions
+ * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID}
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The primaries array is null or has a length that is neither 6 or 9.
+ * - The white point array is null or has a length that is neither 2 or 3.
+ * - The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
+ * - The transfer parameters are invalid.
+ *
+ *
+ * @see #get(Named)
+ */
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @Nullable @Size(9) float[] transform,
+ @NonNull TransferParameters function,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ this(name, primaries, whitePoint, transform,
+ function.e == 0.0 && function.f == 0.0 ?
+ x -> rcpResponse(x, function.a, function.b,
+ function.c, function.d, function.g) :
+ x -> rcpResponse(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g),
+ function.e == 0.0 && function.f == 0.0 ?
+ x -> response(x, function.a, function.b,
+ function.c, function.d, function.g) :
+ x -> response(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g),
+ 0.0f, 1.0f, function, id);
+ }
+ /**
+ * Creates a new RGB color space using a 3x3 column-major transform matrix.
+ * The transform matrix must convert from the RGB space to the profile connection
+ * space CIE XYZ.
+ *
+ * The range of the color space is imposed to be \([0..1]\).
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param toXYZ 3x3 column-major transform matrix from RGB to the profile
+ * connection space CIE XYZ as an array of 9 floats, cannot be null
+ * @param gamma Gamma to use as the transfer function
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - Gamma is negative.
+ *
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(9) float[] toXYZ,
+ double gamma) {
+ this(name, computePrimaries(toXYZ), computeWhitePoint(toXYZ), gamma, 0.0f, 1.0f, MIN_ID);
+ }
+ /**
+ * Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.
+ *
+ * The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ *
+ *
+ * Space | Primaries length | White point length |
+ * xyY | 6 | 2 |
+ * XYZ | 9 | 3 |
+ *
+ *
+ * When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param gamma Gamma to use as the transfer function
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The primaries array is null or has a length that is neither 6 or 9.
+ * - The white point array is null or has a length that is neither 2 or 3.
+ * - Gamma is negative.
+ *
+ *
+ * @see #get(Named)
+ */
+ public Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ double gamma) {
+ this(name, primaries, whitePoint, gamma, 0.0f, 1.0f, MIN_ID);
+ }
+ /**
+ * Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.
+ *
+ * The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ *
+ *
+ * Space | Primaries length | White point length |
+ * xyY | 6 | 2 |
+ * XYZ | 9 | 3 |
+ *
+ *
+ * When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param gamma Gamma to use as the transfer function
+ * @param min The minimum valid value in this color space's RGB range
+ * @param max The maximum valid value in this color space's RGB range
+ * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID}
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The primaries array is null or has a length that is neither 6 or 9.
+ * - The white point array is null or has a length that is neither 2 or 3.
+ * - The minimum valid value is >= the maximum valid value.
+ * - The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
+ * - Gamma is negative.
+ *
+ *
+ * @see #get(Named)
+ */
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ double gamma,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ this(name, primaries, whitePoint, null,
+ gamma == 1.0 ? DoubleUnaryOperator.identity() :
+ x -> Math.pow(x < 0.0 ? 0.0 : x, 1 / gamma),
+ gamma == 1.0 ? DoubleUnaryOperator.identity() :
+ x -> Math.pow(x < 0.0 ? 0.0 : x, gamma),
+ min, max, new TransferParameters(1.0, 0.0, 0.0, 0.0, gamma), id);
+ }
+ /**
+ * Creates a new RGB color space using a specified set of primaries
+ * and a specified white point.
+ *
+ * The primaries and white point can be specified in the CIE xyY space
+ * or in CIE XYZ. The length of the arrays depends on the chosen space:
+ *
+ *
+ * Space | Primaries length | White point length |
+ * xyY | 6 | 2 |
+ * XYZ | 9 | 3 |
+ *
+ *
+ * When the primaries and/or white point are specified in xyY, the Y component
+ * does not need to be specified and is assumed to be 1.0. Only the xy components
+ * are required.
+ *
+ * @param name Name of the color space, cannot be null, its length must be >= 1
+ * @param primaries RGB primaries as an array of 6 (xy) or 9 (XYZ) floats
+ * @param whitePoint Reference white as an array of 2 (xy) or 3 (XYZ) floats
+ * @param transform Computed transform matrix that converts from RGB to XYZ, or
+ * {@code null} to compute it from {@code primaries} and {@code whitePoint}.
+ * @param oetf Opto-electronic transfer function, cannot be null
+ * @param eotf Electro-optical transfer function, cannot be null
+ * @param min The minimum valid value in this color space's RGB range
+ * @param max The maximum valid value in this color space's RGB range
+ * @param transferParameters Parameters for the transfer functions
+ * @param id ID of this color space as an integer between {@link #MIN_ID} and {@link #MAX_ID}
+ *
+ * @throws IllegalArgumentException If any of the following conditions is met:
+ *
+ * - The name is null or has a length of 0.
+ * - The primaries array is null or has a length that is neither 6 or 9.
+ * - The white point array is null or has a length that is neither 2 or 3.
+ * - The OETF is null or the EOTF is null.
+ * - The minimum valid value is >= the maximum valid value.
+ * - The ID is not between {@link #MIN_ID} and {@link #MAX_ID}.
+ *
+ *
+ * @see #get(Named)
+ */
+ private Rgb(
+ @NonNull @Size(min = 1) String name,
+ @NonNull @Size(min = 6, max = 9) float[] primaries,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint,
+ @Nullable @Size(9) float[] transform,
+ @NonNull DoubleUnaryOperator oetf,
+ @NonNull DoubleUnaryOperator eotf,
+ float min,
+ float max,
+ @Nullable TransferParameters transferParameters,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ super(name, Model.RGB, id);
+ if (primaries == null || (primaries.length != 6 && primaries.length != 9)) {
+ throw new IllegalArgumentException("The color space's primaries must be " +
+ "defined as an array of 6 floats in xyY or 9 floats in XYZ");
+ }
+ if (whitePoint == null || (whitePoint.length != 2 && whitePoint.length != 3)) {
+ throw new IllegalArgumentException("The color space's white point must be " +
+ "defined as an array of 2 floats in xyY or 3 float in XYZ");
+ }
+ if (oetf == null || eotf == null) {
+ throw new IllegalArgumentException("The transfer functions of a color space " +
+ "cannot be null");
+ }
+ if (min >= max) {
+ throw new IllegalArgumentException("Invalid range: min=" + min + ", max=" + max +
+ "; min must be strictly < max");
+ }
+ mWhitePoint = xyWhitePoint(whitePoint);
+ mPrimaries = xyPrimaries(primaries);
+ if (transform == null) {
+ mTransform = computeXYZMatrix(mPrimaries, mWhitePoint);
+ } else {
+ if (transform.length != 9) {
+ throw new IllegalArgumentException("Transform must have 9 entries! Has "
+ + transform.length);
+ }
+ mTransform = transform;
+ }
+ mInverseTransform = inverse3x3(mTransform);
+ mOetf = oetf;
+ mEotf = eotf;
+ mMin = min;
+ mMax = max;
+ DoubleUnaryOperator clamp = this::clamp;
+ mClampedOetf = oetf.andThen(clamp);
+ mClampedEotf = clamp.andThen(eotf);
+ mTransferParameters = transferParameters;
+ // A color space is wide-gamut if its area is >90% of NTSC 1953 and
+ // if it entirely contains the Color space definition in xyY
+ mIsWideGamut = isWideGamut(mPrimaries, min, max);
+ mIsSrgb = isSrgb(mPrimaries, mWhitePoint, oetf, eotf, min, max, id);
+
+ mNativePtr = 0;
+ }
+ /**
+ * Creates a copy of the specified color space with a new transform.
+ *
+ * @param colorSpace The color space to create a copy of
+ */
+ private Rgb(Rgb colorSpace,
+ @NonNull @Size(9) float[] transform,
+ @NonNull @Size(min = 2, max = 3) float[] whitePoint) {
+ this(colorSpace.getName(), colorSpace.mPrimaries, whitePoint, transform,
+ colorSpace.mOetf, colorSpace.mEotf, colorSpace.mMin, colorSpace.mMax,
+ colorSpace.mTransferParameters, MIN_ID);
+ }
+ /**
+ * Copies the non-adapted CIE xyY white point of this color space in
+ * specified array. The Y component is assumed to be 1 and is therefore
+ * not copied into the destination. The x and y components are written
+ * in the array at positions 0 and 1 respectively.
+ *
+ * @param whitePoint The destination array, cannot be null, its length
+ * must be >= 2
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getWhitePoint()
+ */
+ @NonNull
+ @Size(min = 2)
+ public float[] getWhitePoint(@NonNull @Size(min = 2) float[] whitePoint) {
+ whitePoint[0] = mWhitePoint[0];
+ whitePoint[1] = mWhitePoint[1];
+ return whitePoint;
+ }
+ /**
+ * Returns the non-adapted CIE xyY white point of this color space as
+ * a new array of 2 floats. The Y component is assumed to be 1 and is
+ * therefore not copied into the destination. The x and y components
+ * are written in the array at positions 0 and 1 respectively.
+ *
+ * @return A new non-null array of 2 floats
+ *
+ * @see #getWhitePoint(float[])
+ */
+ @NonNull
+ @Size(2)
+ public float[] getWhitePoint() {
+ return Arrays.copyOf(mWhitePoint, mWhitePoint.length);
+ }
+ /**
+ * Copies the primaries of this color space in specified array. The Y
+ * component is assumed to be 1 and is therefore not copied into the
+ * destination. The x and y components of the first primary are written
+ * in the array at positions 0 and 1 respectively.
+ *
+ * Note: Some ColorSpaces represent gray profiles. The concept of
+ * primaries for such a ColorSpace does not make sense, so we use a special
+ * set of primaries that are all 1s.
+ *
+ * @param primaries The destination array, cannot be null, its length
+ * must be >= 6
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getPrimaries()
+ */
+ @NonNull
+ @Size(min = 6)
+ public float[] getPrimaries(@NonNull @Size(min = 6) float[] primaries) {
+ System.arraycopy(mPrimaries, 0, primaries, 0, mPrimaries.length);
+ return primaries;
+ }
+ /**
+ * Returns the primaries of this color space as a new array of 6 floats.
+ * The Y component is assumed to be 1 and is therefore not copied into
+ * the destination. The x and y components of the first primary are
+ * written in the array at positions 0 and 1 respectively.
+ *
+ * Note: Some ColorSpaces represent gray profiles. The concept of
+ * primaries for such a ColorSpace does not make sense, so we use a special
+ * set of primaries that are all 1s.
+ *
+ * @return A new non-null array of 2 floats
+ *
+ * @see #getPrimaries(float[])
+ */
+ @NonNull
+ @Size(6)
+ public float[] getPrimaries() {
+ return Arrays.copyOf(mPrimaries, mPrimaries.length);
+ }
+ /**
+ * Copies the transform of this color space in specified array. The
+ * transform is used to convert from RGB to XYZ (with the same white
+ * point as this color space). To connect color spaces, you must first
+ * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the
+ * same white point.
+ * It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.
+ *
+ * @param transform The destination array, cannot be null, its length
+ * must be >= 9
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getTransform()
+ */
+ @NonNull
+ @Size(min = 9)
+ public float[] getTransform(@NonNull @Size(min = 9) float[] transform) {
+ System.arraycopy(mTransform, 0, transform, 0, mTransform.length);
+ return transform;
+ }
+ /**
+ * Returns the transform of this color space as a new array. The
+ * transform is used to convert from RGB to XYZ (with the same white
+ * point as this color space). To connect color spaces, you must first
+ * {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them to the
+ * same white point.
+ * It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.
+ *
+ * @return A new array of 9 floats
+ *
+ * @see #getTransform(float[])
+ */
+ @NonNull
+ @Size(9)
+ public float[] getTransform() {
+ return Arrays.copyOf(mTransform, mTransform.length);
+ }
+ /**
+ * Copies the inverse transform of this color space in specified array.
+ * The inverse transform is used to convert from XYZ to RGB (with the
+ * same white point as this color space). To connect color spaces, you
+ * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them
+ * to the same white point.
+ * It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.
+ *
+ * @param inverseTransform The destination array, cannot be null, its length
+ * must be >= 9
+ *
+ * @return The destination array passed as a parameter
+ *
+ * @see #getInverseTransform()
+ */
+ @NonNull
+ @Size(min = 9)
+ public float[] getInverseTransform(@NonNull @Size(min = 9) float[] inverseTransform) {
+ System.arraycopy(mInverseTransform, 0, inverseTransform, 0, mInverseTransform.length);
+ return inverseTransform;
+ }
+ /**
+ * Returns the inverse transform of this color space as a new array.
+ * The inverse transform is used to convert from XYZ to RGB (with the
+ * same white point as this color space). To connect color spaces, you
+ * must first {@link ColorSpace#adapt(ColorSpace, float[]) adapt} them
+ * to the same white point.
+ * It is recommended to use {@link ColorSpace#connect(ColorSpace, ColorSpace)}
+ * to convert between color spaces.
+ *
+ * @return A new array of 9 floats
+ *
+ * @see #getInverseTransform(float[])
+ */
+ @NonNull
+ @Size(9)
+ public float[] getInverseTransform() {
+ return Arrays.copyOf(mInverseTransform, mInverseTransform.length);
+ }
+ /**
+ * Returns the opto-electronic transfer function (OETF) of this color space.
+ * The inverse function is the electro-optical transfer function (EOTF) returned
+ * by {@link #getEotf()}. These functions are defined to satisfy the following
+ * equality for \(x \in [0..1]\):
+ *
+ * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$
+ *
+ * For RGB colors, this function can be used to convert from linear space
+ * to "gamma space" (gamma encoded). The terms gamma space and gamma encoded
+ * are frequently used because many OETFs can be closely approximated using
+ * a simple power function of the form \(x^{\frac{1}{\gamma}}\) (the
+ * approximation of the {@link Named#SRGB sRGB} OETF uses \(\gamma=2.2\)
+ * for instance).
+ *
+ * @return A transfer function that converts from linear space to "gamma space"
+ *
+ * @see #getEotf()
+ * @see #getTransferParameters()
+ */
+ @NonNull
+ public DoubleUnaryOperator getOetf() {
+ return mClampedOetf;
+ }
+ /**
+ * Returns the electro-optical transfer function (EOTF) of this color space.
+ * The inverse function is the opto-electronic transfer function (OETF)
+ * returned by {@link #getOetf()}. These functions are defined to satisfy the
+ * following equality for \(x \in [0..1]\):
+ *
+ * $$OETF(EOTF(x)) = EOTF(OETF(x)) = x$$
+ *
+ * For RGB colors, this function can be used to convert from "gamma space"
+ * (gamma encoded) to linear space. The terms gamma space and gamma encoded
+ * are frequently used because many EOTFs can be closely approximated using
+ * a simple power function of the form \(x^\gamma\) (the approximation of the
+ * {@link Named#SRGB sRGB} EOTF uses \(\gamma=2.2\) for instance).
+ *
+ * @return A transfer function that converts from "gamma space" to linear space
+ *
+ * @see #getOetf()
+ * @see #getTransferParameters()
+ */
+ @NonNull
+ public DoubleUnaryOperator getEotf() {
+ return mClampedEotf;
+ }
+ /**
+ * Returns the parameters used by the {@link #getEotf() electro-optical}
+ * and {@link #getOetf() opto-electronic} transfer functions. If the transfer
+ * functions do not match the ICC parametric curves defined in ICC.1:2004-10
+ * (section 10.15), this method returns null.
+ *
+ * See {@link TransferParameters} for a full description of the transfer
+ * functions.
+ *
+ * @return An instance of {@link TransferParameters} or null if this color
+ * space's transfer functions do not match the equation defined in
+ * {@link TransferParameters}
+ */
+ @Nullable
+ public TransferParameters getTransferParameters() {
+ return mTransferParameters;
+ }
+ @Override
+ public boolean isSrgb() {
+ return mIsSrgb;
+ }
+ @Override
+ public boolean isWideGamut() {
+ return mIsWideGamut;
+ }
+ @Override
+ public float getMinValue(int component) {
+ return mMin;
+ }
+ @Override
+ public float getMaxValue(int component) {
+ return mMax;
+ }
+ /**
+ * Decodes an RGB value to linear space. This is achieved by
+ * applying this color space's electro-optical transfer function
+ * to the supplied values.
+ *
+ * Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.
+ *
+ * @param r The red component to decode to linear space
+ * @param g The green component to decode to linear space
+ * @param b The blue component to decode to linear space
+ * @return A new array of 3 floats containing linear RGB values
+ *
+ * @see #toLinear(float[])
+ * @see #fromLinear(float, float, float)
+ */
+ @NonNull
+ @Size(3)
+ public float[] toLinear(float r, float g, float b) {
+ return toLinear(new float[] { r, g, b });
+ }
+ /**
+ * Decodes an RGB value to linear space. This is achieved by
+ * applying this color space's electro-optical transfer function
+ * to the first 3 values of the supplied array. The result is
+ * stored back in the input array.
+ *
+ * Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.
+ *
+ * @param v A non-null array of non-linear RGB values, its length
+ * must be at least 3
+ * @return The specified array
+ *
+ * @see #toLinear(float, float, float)
+ * @see #fromLinear(float[])
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] toLinear(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedEotf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedEotf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedEotf.applyAsDouble(v[2]);
+ return v;
+ }
+ /**
+ * Encodes an RGB value from linear space to this color space's
+ * "gamma space". This is achieved by applying this color space's
+ * opto-electronic transfer function to the supplied values.
+ *
+ * Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.
+ *
+ * @param r The red component to encode from linear space
+ * @param g The green component to encode from linear space
+ * @param b The blue component to encode from linear space
+ * @return A new array of 3 floats containing non-linear RGB values
+ *
+ * @see #fromLinear(float[])
+ * @see #toLinear(float, float, float)
+ */
+ @NonNull
+ @Size(3)
+ public float[] fromLinear(float r, float g, float b) {
+ return fromLinear(new float[] { r, g, b });
+ }
+ /**
+ * Encodes an RGB value from linear space to this color space's
+ * "gamma space". This is achieved by applying this color space's
+ * opto-electronic transfer function to the first 3 values of the
+ * supplied array. The result is stored back in the input array.
+ *
+ * Refer to the documentation of {@link ColorSpace.Rgb} for
+ * more information about transfer functions and their use for
+ * encoding and decoding RGB values.
+ *
+ * @param v A non-null array of linear RGB values, its length
+ * must be at least 3
+ * @return A new array of 3 floats containing non-linear RGB values
+ *
+ * @see #fromLinear(float[])
+ * @see #toLinear(float, float, float)
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] fromLinear(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedOetf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedOetf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedOetf.applyAsDouble(v[2]);
+ return v;
+ }
+ @Override
+ @NonNull
+ @Size(min = 3)
+ public float[] toXyz(@NonNull @Size(min = 3) float[] v) {
+ v[0] = (float) mClampedEotf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedEotf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedEotf.applyAsDouble(v[2]);
+ return mul3x3Float3(mTransform, v);
+ }
+ @Override
+ @NonNull
+ @Size(min = 3)
+ public float[] fromXyz(@NonNull @Size(min = 3) float[] v) {
+ mul3x3Float3(mInverseTransform, v);
+ v[0] = (float) mClampedOetf.applyAsDouble(v[0]);
+ v[1] = (float) mClampedOetf.applyAsDouble(v[1]);
+ v[2] = (float) mClampedOetf.applyAsDouble(v[2]);
+ return v;
+ }
+ private double clamp(double x) {
+ return x < mMin ? mMin : x > mMax ? mMax : x;
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ Rgb rgb = (Rgb) o;
+ if (Float.compare(rgb.mMin, mMin) != 0) return false;
+ if (Float.compare(rgb.mMax, mMax) != 0) return false;
+ if (!Arrays.equals(mWhitePoint, rgb.mWhitePoint)) return false;
+ if (!Arrays.equals(mPrimaries, rgb.mPrimaries)) return false;
+ if (mTransferParameters != null) {
+ return mTransferParameters.equals(rgb.mTransferParameters);
+ } else if (rgb.mTransferParameters == null) {
+ return true;
+ }
+ //noinspection SimplifiableIfStatement
+ if (!mOetf.equals(rgb.mOetf)) return false;
+ return mEotf.equals(rgb.mEotf);
+ }
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + Arrays.hashCode(mWhitePoint);
+ result = 31 * result + Arrays.hashCode(mPrimaries);
+ result = 31 * result + (mMin != +0.0f ? Float.floatToIntBits(mMin) : 0);
+ result = 31 * result + (mMax != +0.0f ? Float.floatToIntBits(mMax) : 0);
+ result = 31 * result +
+ (mTransferParameters != null ? mTransferParameters.hashCode() : 0);
+ if (mTransferParameters == null) {
+ result = 31 * result + mOetf.hashCode();
+ result = 31 * result + mEotf.hashCode();
+ }
+ return result;
+ }
+ /**
+ * Computes whether a color space is the sRGB color space or at least
+ * a close approximation.
+ *
+ * @param primaries The set of RGB primaries in xyY as an array of 6 floats
+ * @param whitePoint The white point in xyY as an array of 2 floats
+ * @param OETF The opto-electronic transfer function
+ * @param EOTF The electro-optical transfer function
+ * @param min The minimum value of the color space's range
+ * @param max The minimum value of the color space's range
+ * @param id The ID of the color space
+ * @return True if the color space can be considered as the sRGB color space
+ *
+ * @see #isSrgb()
+ */
+ @SuppressWarnings("RedundantIfStatement")
+ private static boolean isSrgb(
+ @NonNull @Size(6) float[] primaries,
+ @NonNull @Size(2) float[] whitePoint,
+ @NonNull DoubleUnaryOperator OETF,
+ @NonNull DoubleUnaryOperator EOTF,
+ float min,
+ float max,
+ @IntRange(from = MIN_ID, to = MAX_ID) int id) {
+ if (id == 0) return true;
+ if (!ColorSpace.compare(primaries, SRGB_PRIMARIES)) {
+ return false;
+ }
+ if (!ColorSpace.compare(whitePoint, ILLUMINANT_D65)) {
+ return false;
+ }
+ if (min != 0.0f) return false;
+ if (max != 1.0f) return false;
+ // We would have already returned true if this was SRGB itself, so
+ // it is safe to reference it here.
+ ColorSpace.Rgb srgb = (ColorSpace.Rgb) get(Named.SRGB);
+ for (double x = 0.0; x <= 1.0; x += 1 / 255.0) {
+ if (!compare(x, OETF, srgb.mOetf)) return false;
+ if (!compare(x, EOTF, srgb.mEotf)) return false;
+ }
+ return true;
+ }
+ /**
+ * Report whether this matrix is a special gray matrix.
+ * @param toXYZ A XYZD50 matrix. Skia uses a special form for a gray profile.
+ * @return true if this is a special gray matrix.
+ */
+ private static boolean isGray(@NonNull @Size(9) float[] toXYZ) {
+ return toXYZ.length == 9 && toXYZ[1] == 0 && toXYZ[2] == 0 && toXYZ[3] == 0
+ && toXYZ[5] == 0 && toXYZ[6] == 0 && toXYZ[7] == 0;
+ }
+ private static boolean compare(double point, @NonNull DoubleUnaryOperator a,
+ @NonNull DoubleUnaryOperator b) {
+ double rA = a.applyAsDouble(point);
+ double rB = b.applyAsDouble(point);
+ return Math.abs(rA - rB) <= 1e-3;
+ }
+ /**
+ * Computes whether the specified CIE xyY or XYZ primaries (with Y set to 1) form
+ * a wide color gamut. A color gamut is considered wide if its area is > 90%
+ * of the area of NTSC 1953 and if it contains the sRGB color gamut entirely.
+ * If the conditions above are not met, the color space is considered as having
+ * a wide color gamut if its range is larger than [0..1].
+ *
+ * @param primaries RGB primaries in CIE xyY as an array of 6 floats
+ * @param min The minimum value of the color space's range
+ * @param max The minimum value of the color space's range
+ * @return True if the color space has a wide gamut, false otherwise
+ *
+ * @see #isWideGamut()
+ * @see #area(float[])
+ */
+ private static boolean isWideGamut(@NonNull @Size(6) float[] primaries,
+ float min, float max) {
+ return (area(primaries) / area(NTSC_1953_PRIMARIES) > 0.9f &&
+ contains(primaries, SRGB_PRIMARIES)) || (min < 0.0f && max > 1.0f);
+ }
+ /**
+ * Computes the area of the triangle represented by a set of RGB primaries
+ * in the CIE xyY space.
+ *
+ * @param primaries The triangle's vertices, as RGB primaries in an array of 6 floats
+ * @return The area of the triangle
+ *
+ * @see #isWideGamut(float[], float, float)
+ */
+ private static float area(@NonNull @Size(6) float[] primaries) {
+ float Rx = primaries[0];
+ float Ry = primaries[1];
+ float Gx = primaries[2];
+ float Gy = primaries[3];
+ float Bx = primaries[4];
+ float By = primaries[5];
+ float det = Rx * Gy + Ry * Bx + Gx * By - Gy * Bx - Ry * Gx - Rx * By;
+ float r = 0.5f * det;
+ return r < 0.0f ? -r : r;
+ }
+ /**
+ * Computes the cross product of two 2D vectors.
+ *
+ * @param ax The x coordinate of the first vector
+ * @param ay The y coordinate of the first vector
+ * @param bx The x coordinate of the second vector
+ * @param by The y coordinate of the second vector
+ * @return The result of a x b
+ */
+ private static float cross(float ax, float ay, float bx, float by) {
+ return ax * by - ay * bx;
+ }
+ /**
+ * Decides whether a 2D triangle, identified by the 6 coordinates of its
+ * 3 vertices, is contained within another 2D triangle, also identified
+ * by the 6 coordinates of its 3 vertices.
+ *
+ * In the illustration below, we want to test whether the RGB triangle
+ * is contained within the triangle XYZ formed by the 3 vertices at
+ * the "+" locations.
+ *
+ * Y .
+ * . + .
+ * . ..
+ * . .
+ * . .
+ * . G
+ * *
+ * * *
+ * ** *
+ * * **
+ * * *
+ * ** *
+ * * *
+ * * *
+ * ** *
+ * * *
+ * * **
+ * ** * R ...
+ * * * .....
+ * * ***** ..
+ * ** ************ . +
+ * B * ************ . X
+ * ......***** .
+ * ...... . .
+ * ..
+ * + .
+ * Z .
+ *
+ * RGB is contained within XYZ if all the following conditions are true
+ * (with "x" the cross product operator):
+ *
+ * --> -->
+ * GR x RX >= 0
+ * --> -->
+ * RX x BR >= 0
+ * --> -->
+ * RG x GY >= 0
+ * --> -->
+ * GY x RG >= 0
+ * --> -->
+ * RB x BZ >= 0
+ * --> -->
+ * BZ x GB >= 0
+ *
+ * @param p1 The enclosing triangle
+ * @param p2 The enclosed triangle
+ * @return True if the triangle p1 contains the triangle p2
+ *
+ * @see #isWideGamut(float[], float, float)
+ */
+ @SuppressWarnings("RedundantIfStatement")
+ private static boolean contains(@NonNull @Size(6) float[] p1, @NonNull @Size(6) float[] p2) {
+ // Translate the vertices p1 in the coordinates system
+ // with the vertices p2 as the origin
+ float[] p0 = new float[] {
+ p1[0] - p2[0], p1[1] - p2[1],
+ p1[2] - p2[2], p1[3] - p2[3],
+ p1[4] - p2[4], p1[5] - p2[5],
+ };
+ // Check the first vertex of p1
+ if (cross(p0[0], p0[1], p2[0] - p2[4], p2[1] - p2[5]) < 0 ||
+ cross(p2[0] - p2[2], p2[1] - p2[3], p0[0], p0[1]) < 0) {
+ return false;
+ }
+ // Check the second vertex of p1
+ if (cross(p0[2], p0[3], p2[2] - p2[0], p2[3] - p2[1]) < 0 ||
+ cross(p2[2] - p2[4], p2[3] - p2[5], p0[2], p0[3]) < 0) {
+ return false;
+ }
+ // Check the third vertex of p1
+ if (cross(p0[4], p0[5], p2[4] - p2[2], p2[5] - p2[3]) < 0 ||
+ cross(p2[4] - p2[0], p2[5] - p2[1], p0[4], p0[5]) < 0) {
+ return false;
+ }
+ return true;
+ }
+ /**
+ * Computes the primaries of a color space identified only by
+ * its RGB->XYZ transform matrix. This method assumes that the
+ * range of the color space is [0..1].
+ *
+ * @param toXYZ The color space's 3x3 transform matrix to XYZ
+ * @return A new array of 6 floats containing the color space's
+ * primaries in CIE xyY
+ */
+ @NonNull
+ @Size(6)
+ private static float[] computePrimaries(@NonNull @Size(9) float[] toXYZ) {
+ float[] r = mul3x3Float3(toXYZ, new float[] { 1.0f, 0.0f, 0.0f });
+ float[] g = mul3x3Float3(toXYZ, new float[] { 0.0f, 1.0f, 0.0f });
+ float[] b = mul3x3Float3(toXYZ, new float[] { 0.0f, 0.0f, 1.0f });
+ float rSum = r[0] + r[1] + r[2];
+ float gSum = g[0] + g[1] + g[2];
+ float bSum = b[0] + b[1] + b[2];
+ return new float[] {
+ r[0] / rSum, r[1] / rSum,
+ g[0] / gSum, g[1] / gSum,
+ b[0] / bSum, b[1] / bSum,
+ };
+ }
+ /**
+ * Computes the white point of a color space identified only by
+ * its RGB->XYZ transform matrix. This method assumes that the
+ * range of the color space is [0..1].
+ *
+ * @param toXYZ The color space's 3x3 transform matrix to XYZ
+ * @return A new array of 2 floats containing the color space's
+ * white point in CIE xyY
+ */
+ @NonNull
+ @Size(2)
+ private static float[] computeWhitePoint(@NonNull @Size(9) float[] toXYZ) {
+ float[] w = mul3x3Float3(toXYZ, new float[] { 1.0f, 1.0f, 1.0f });
+ float sum = w[0] + w[1] + w[2];
+ return new float[] { w[0] / sum, w[1] / sum };
+ }
+ /**
+ * Converts the specified RGB primaries point to xyY if needed. The primaries
+ * can be specified as an array of 6 floats (in CIE xyY) or 9 floats
+ * (in CIE XYZ). If no conversion is needed, the input array is copied.
+ *
+ * @param primaries The primaries in xyY or XYZ
+ * @return A new array of 6 floats containing the primaries in xyY
+ */
+ @NonNull
+ @Size(6)
+ private static float[] xyPrimaries(@NonNull @Size(min = 6, max = 9) float[] primaries) {
+ float[] xyPrimaries = new float[6];
+ // XYZ to xyY
+ if (primaries.length == 9) {
+ float sum;
+ sum = primaries[0] + primaries[1] + primaries[2];
+ xyPrimaries[0] = primaries[0] / sum;
+ xyPrimaries[1] = primaries[1] / sum;
+ sum = primaries[3] + primaries[4] + primaries[5];
+ xyPrimaries[2] = primaries[3] / sum;
+ xyPrimaries[3] = primaries[4] / sum;
+ sum = primaries[6] + primaries[7] + primaries[8];
+ xyPrimaries[4] = primaries[6] / sum;
+ xyPrimaries[5] = primaries[7] / sum;
+ } else {
+ System.arraycopy(primaries, 0, xyPrimaries, 0, 6);
+ }
+ return xyPrimaries;
+ }
+ /**
+ * Converts the specified white point to xyY if needed. The white point
+ * can be specified as an array of 2 floats (in CIE xyY) or 3 floats
+ * (in CIE XYZ). If no conversion is needed, the input array is copied.
+ *
+ * @param whitePoint The white point in xyY or XYZ
+ * @return A new array of 2 floats containing the white point in xyY
+ */
+ @NonNull
+ @Size(2)
+ private static float[] xyWhitePoint(@Size(min = 2, max = 3) float[] whitePoint) {
+ float[] xyWhitePoint = new float[2];
+ // XYZ to xyY
+ if (whitePoint.length == 3) {
+ float sum = whitePoint[0] + whitePoint[1] + whitePoint[2];
+ xyWhitePoint[0] = whitePoint[0] / sum;
+ xyWhitePoint[1] = whitePoint[1] / sum;
+ } else {
+ System.arraycopy(whitePoint, 0, xyWhitePoint, 0, 2);
+ }
+ return xyWhitePoint;
+ }
+ /**
+ * Computes the matrix that converts from RGB to XYZ based on RGB
+ * primaries and a white point, both specified in the CIE xyY space.
+ * The Y component of the primaries and white point is implied to be 1.
+ *
+ * @param primaries The RGB primaries in xyY, as an array of 6 floats
+ * @param whitePoint The white point in xyY, as an array of 2 floats
+ * @return A 3x3 matrix as a new array of 9 floats
+ */
+ @NonNull
+ @Size(9)
+ private static float[] computeXYZMatrix(
+ @NonNull @Size(6) float[] primaries,
+ @NonNull @Size(2) float[] whitePoint) {
+ float Rx = primaries[0];
+ float Ry = primaries[1];
+ float Gx = primaries[2];
+ float Gy = primaries[3];
+ float Bx = primaries[4];
+ float By = primaries[5];
+ float Wx = whitePoint[0];
+ float Wy = whitePoint[1];
+ float oneRxRy = (1 - Rx) / Ry;
+ float oneGxGy = (1 - Gx) / Gy;
+ float oneBxBy = (1 - Bx) / By;
+ float oneWxWy = (1 - Wx) / Wy;
+ float RxRy = Rx / Ry;
+ float GxGy = Gx / Gy;
+ float BxBy = Bx / By;
+ float WxWy = Wx / Wy;
+ float BY =
+ ((oneWxWy - oneRxRy) * (GxGy - RxRy) - (WxWy - RxRy) * (oneGxGy - oneRxRy)) /
+ ((oneBxBy - oneRxRy) * (GxGy - RxRy) - (BxBy - RxRy) * (oneGxGy - oneRxRy));
+ float GY = (WxWy - RxRy - BY * (BxBy - RxRy)) / (GxGy - RxRy);
+ float RY = 1 - GY - BY;
+ float RYRy = RY / Ry;
+ float GYGy = GY / Gy;
+ float BYBy = BY / By;
+ return new float[] {
+ RYRy * Rx, RY, RYRy * (1 - Rx - Ry),
+ GYGy * Gx, GY, GYGy * (1 - Gx - Gy),
+ BYBy * Bx, BY, BYBy * (1 - Bx - By)
+ };
+ }
+ }
+ /**
+ * {@usesMathJax}
+ *
+ * A connector transforms colors from a source color space to a destination
+ * color space.
+ *
+ * A source color space is connected to a destination color space using the
+ * color transform \(C\) computed from their respective transforms noted
+ * \(T_{src}\) and \(T_{dst}\) in the following equation:
+ *
+ * $$C = T^{-1}_{dst} . T_{src}$$
+ *
+ * The transform \(C\) shown above is only valid when the source and
+ * destination color spaces have the same profile connection space (PCS).
+ * We know that instances of {@link ColorSpace} always use CIE XYZ as their
+ * PCS but their white points might differ. When they do, we must perform
+ * a chromatic adaptation of the color spaces' transforms. To do so, we
+ * use the von Kries method described in the documentation of {@link Adaptation},
+ * using the CIE standard illuminant {@link ColorSpace#ILLUMINANT_D50 D50}
+ * as the target white point.
+ *
+ * Example of conversion from {@link Named#SRGB sRGB} to
+ * {@link Named#DCI_P3 DCI-P3}:
+ *
+ *
+ * ColorSpace.Connector connector = ColorSpace.connect(
+ * ColorSpace.get(ColorSpace.Named.SRGB),
+ * ColorSpace.get(ColorSpace.Named.DCI_P3));
+ * float[] p3 = connector.transform(1.0f, 0.0f, 0.0f);
+ * // p3 contains { 0.9473, 0.2740, 0.2076 }
+ *
+ *
+ * @see Adaptation
+ * @see ColorSpace#adapt(ColorSpace, float[], Adaptation)
+ * @see ColorSpace#adapt(ColorSpace, float[])
+ * @see ColorSpace#connect(ColorSpace, ColorSpace, RenderIntent)
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ * @see ColorSpace#connect(ColorSpace, RenderIntent)
+ * @see ColorSpace#connect(ColorSpace)
+ */
+ @AnyThread
+ public static class Connector {
+ @NonNull private final ColorSpace mSource;
+ @NonNull private final ColorSpace mDestination;
+ @NonNull private final ColorSpace mTransformSource;
+ @NonNull private final ColorSpace mTransformDestination;
+ @NonNull private final RenderIntent mIntent;
+ @NonNull @Size(3) private final float[] mTransform;
+ /**
+ * Creates a new connector between a source and a destination color space.
+ *
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @param intent The render intent to use when compressing gamuts
+ */
+ Connector(@NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull RenderIntent intent) {
+ this(source, destination,
+ source.getModel() == Model.RGB ? adapt(source, ILLUMINANT_D50_XYZ) : source,
+ destination.getModel() == Model.RGB ?
+ adapt(destination, ILLUMINANT_D50_XYZ) : destination,
+ intent, computeTransform(source, destination, intent));
+ }
+ /**
+ * To connect between color spaces, we might need to use adapted transforms.
+ * This should be transparent to the user so this constructor takes the
+ * original source and destinations (returned by the getters), as well as
+ * possibly adapted color spaces used by transform().
+ */
+ private Connector(
+ @NonNull ColorSpace source, @NonNull ColorSpace destination,
+ @NonNull ColorSpace transformSource, @NonNull ColorSpace transformDestination,
+ @NonNull RenderIntent intent, @Nullable @Size(3) float[] transform) {
+ mSource = source;
+ mDestination = destination;
+ mTransformSource = transformSource;
+ mTransformDestination = transformDestination;
+ mIntent = intent;
+ mTransform = transform;
+ }
+ /**
+ * Computes an extra transform to apply in XYZ space depending on the
+ * selected rendering intent.
+ */
+ @Nullable
+ private static float[] computeTransform(@NonNull ColorSpace source,
+ @NonNull ColorSpace destination, @NonNull RenderIntent intent) {
+ if (intent != RenderIntent.ABSOLUTE) return null;
+ boolean srcRGB = source.getModel() == Model.RGB;
+ boolean dstRGB = destination.getModel() == Model.RGB;
+ if (srcRGB && dstRGB) return null;
+ if (srcRGB || dstRGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) (srcRGB ? source : destination);
+ float[] srcXYZ = srcRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ float[] dstXYZ = dstRGB ? xyYToXyz(rgb.mWhitePoint) : ILLUMINANT_D50_XYZ;
+ return new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ };
+ }
+ return null;
+ }
+ /**
+ * Returns the source color space this connector will convert from.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ *
+ * @see #getDestination()
+ */
+ @NonNull
+ public ColorSpace getSource() {
+ return mSource;
+ }
+ /**
+ * Returns the destination color space this connector will convert to.
+ *
+ * @return A non-null instance of {@link ColorSpace}
+ *
+ * @see #getSource()
+ */
+ @NonNull
+ public ColorSpace getDestination() {
+ return mDestination;
+ }
+ /**
+ * Returns the render intent this connector will use when mapping the
+ * source color space to the destination color space.
+ *
+ * @return A non-null {@link RenderIntent}
+ *
+ * @see RenderIntent
+ */
+ public RenderIntent getRenderIntent() {
+ return mIntent;
+ }
+ /**
+ * Transforms the specified color from the source color space
+ * to a color in the destination color space. This convenience
+ * method assumes a source color model with 3 components
+ * (typically RGB). To transform from color models with more than
+ * 3 components, such as {@link Model#CMYK CMYK}, use
+ * {@link #transform(float[])} instead.
+ *
+ * @param r The red component of the color to transform
+ * @param g The green component of the color to transform
+ * @param b The blue component of the color to transform
+ * @return A new array of 3 floats containing the specified color
+ * transformed from the source space to the destination space
+ *
+ * @see #transform(float[])
+ */
+ @NonNull
+ @Size(3)
+ public float[] transform(float r, float g, float b) {
+ return transform(new float[] { r, g, b });
+ }
+ /**
+ * Transforms the specified color from the source color space
+ * to a color in the destination color space.
+ *
+ * @param v A non-null array of 3 floats containing the value to transform
+ * and that will hold the result of the transform
+ * @return The v array passed as a parameter, containing the specified color
+ * transformed from the source space to the destination space
+ *
+ * @see #transform(float, float, float)
+ */
+ @NonNull
+ @Size(min = 3)
+ public float[] transform(@NonNull @Size(min = 3) float[] v) {
+ float[] xyz = mTransformSource.toXyz(v);
+ if (mTransform != null) {
+ xyz[0] *= mTransform[0];
+ xyz[1] *= mTransform[1];
+ xyz[2] *= mTransform[2];
+ }
+ return mTransformDestination.fromXyz(xyz);
+ }
+ /**
+ * Optimized connector for RGB->RGB conversions.
+ */
+ private static class Rgb extends Connector {
+ @NonNull private final ColorSpace.Rgb mSource;
+ @NonNull private final ColorSpace.Rgb mDestination;
+ @NonNull private final float[] mTransform;
+ Rgb(@NonNull ColorSpace.Rgb source, @NonNull ColorSpace.Rgb destination,
+ @NonNull RenderIntent intent) {
+ super(source, destination, source, destination, intent, null);
+ mSource = source;
+ mDestination = destination;
+ mTransform = computeTransform(source, destination, intent);
+ }
+ @Override
+ public float[] transform(@NonNull @Size(min = 3) float[] rgb) {
+ rgb[0] = (float) mSource.mClampedEotf.applyAsDouble(rgb[0]);
+ rgb[1] = (float) mSource.mClampedEotf.applyAsDouble(rgb[1]);
+ rgb[2] = (float) mSource.mClampedEotf.applyAsDouble(rgb[2]);
+ mul3x3Float3(mTransform, rgb);
+ rgb[0] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[0]);
+ rgb[1] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[1]);
+ rgb[2] = (float) mDestination.mClampedOetf.applyAsDouble(rgb[2]);
+ return rgb;
+ }
+ /**
+ * Computes the color transform that connects two RGB color spaces.
+ *
+ * We can only connect color spaces if they use the same profile
+ * connection space. We assume the connection space is always
+ * CIE XYZ but we maybe need to perform a chromatic adaptation to
+ * match the white points. If an adaptation is needed, we use the
+ * CIE standard illuminant D50. The unmatched color space is adapted
+ * using the von Kries transform and the {@link Adaptation#BRADFORD}
+ * matrix.
+ *
+ * @param source The source color space, cannot be null
+ * @param destination The destination color space, cannot be null
+ * @param intent The render intent to use when compressing gamuts
+ * @return An array of 9 floats containing the 3x3 matrix transform
+ */
+ @NonNull
+ @Size(9)
+ private static float[] computeTransform(
+ @NonNull ColorSpace.Rgb source,
+ @NonNull ColorSpace.Rgb destination,
+ @NonNull RenderIntent intent) {
+ if (compare(source.mWhitePoint, destination.mWhitePoint)) {
+ // RGB->RGB using the PCS of both color spaces since they have the same
+ return mul3x3(destination.mInverseTransform, source.mTransform);
+ } else {
+ // RGB->RGB using CIE XYZ D50 as the PCS
+ float[] transform = source.mTransform;
+ float[] inverseTransform = destination.mInverseTransform;
+ float[] srcXYZ = xyYToXyz(source.mWhitePoint);
+ float[] dstXYZ = xyYToXyz(destination.mWhitePoint);
+ if (!compare(source.mWhitePoint, ILLUMINANT_D50)) {
+ float[] srcAdaptation = chromaticAdaptation(
+ Adaptation.BRADFORD.mTransform, srcXYZ,
+ Arrays.copyOf(ILLUMINANT_D50_XYZ, 3));
+ transform = mul3x3(srcAdaptation, source.mTransform);
+ }
+ if (!compare(destination.mWhitePoint, ILLUMINANT_D50)) {
+ float[] dstAdaptation = chromaticAdaptation(
+ Adaptation.BRADFORD.mTransform, dstXYZ,
+ Arrays.copyOf(ILLUMINANT_D50_XYZ, 3));
+ inverseTransform = inverse3x3(mul3x3(dstAdaptation, destination.mTransform));
+ }
+ if (intent == RenderIntent.ABSOLUTE) {
+ transform = mul3x3Diag(
+ new float[] {
+ srcXYZ[0] / dstXYZ[0],
+ srcXYZ[1] / dstXYZ[1],
+ srcXYZ[2] / dstXYZ[2],
+ }, transform);
+ }
+ return mul3x3(inverseTransform, transform);
+ }
+ }
+ }
+ /**
+ * Returns the identity connector for a given color space.
+ *
+ * @param source The source and destination color space
+ * @return A non-null connector that does not perform any transform
+ *
+ * @see ColorSpace#connect(ColorSpace, ColorSpace)
+ */
+ static Connector identity(ColorSpace source) {
+ return new Connector(source, source, RenderIntent.RELATIVE) {
+ @Override
+ public float[] transform(@NonNull @Size(min = 3) float[] v) {
+ return v;
+ }
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/android/graphics/FontCache.java b/Vision/src/main/java/android/graphics/FontCache.java
new file mode 100644
index 00000000..052c090c
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/FontCache.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package android.graphics;
+
+import org.jetbrains.skia.Font;
+
+import java.util.HashMap;
+
+class FontCache {
+
+ private static HashMap> cache = new HashMap<>();
+
+ public static Font makeFont(Typeface theTypeface, float textSize) {
+ if(!cache.containsKey(theTypeface)) {
+ cache.put(theTypeface, new HashMap<>());
+ }
+
+ HashMap sizeCache = cache.get(theTypeface);
+
+ if(!sizeCache.containsKey((int) (textSize * 1000))) {
+ sizeCache.put((int) (textSize * 1000), new Font(theTypeface.theTypeface, textSize));
+ }
+
+ return sizeCache.get((int) (textSize * 1000));
+ }
+
+}
diff --git a/Vision/src/main/java/android/graphics/Paint.java b/Vision/src/main/java/android/graphics/Paint.java
new file mode 100644
index 00000000..1aff5285
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Paint.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package android.graphics;
+
+import org.jetbrains.skia.Font;
+import org.jetbrains.skia.FontMetrics;
+import org.jetbrains.skia.PaintStrokeCap;
+import org.jetbrains.skia.PaintStrokeJoin;
+
+public class Paint {
+
+ /*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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.
+ */
+
+
+ /**
+ * The Style specifies if the primitive being drawn is filled, stroked, or
+ * both (in the same color). The default is FILL.
+ */
+ public enum Style {
+ /**
+ * Geometry and text drawn with this style will be filled, ignoring all
+ * stroke-related settings in the paint.
+ */
+ FILL (0),
+ /**
+ * Geometry and text drawn with this style will be stroked, respecting
+ * the stroke-related fields on the paint.
+ */
+ STROKE (1),
+ /**
+ * Geometry and text drawn with this style will be both filled and
+ * stroked at the same time, respecting the stroke-related fields on
+ * the paint. This mode can give unexpected results if the geometry
+ * is oriented counter-clockwise. This restriction does not apply to
+ * either FILL or STROKE.
+ */
+ FILL_AND_STROKE (2);
+ Style(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+ /**
+ * The Cap specifies the treatment for the beginning and ending of
+ * stroked lines and paths. The default is BUTT.
+ */
+ public enum Cap {
+ /**
+ * The stroke ends with the path, and does not project beyond it.
+ */
+ BUTT (0),
+ /**
+ * The stroke projects out as a semicircle, with the center at the
+ * end of the path.
+ */
+ ROUND (1),
+ /**
+ * The stroke projects out as a square, with the center at the end
+ * of the path.
+ */
+ SQUARE (2);
+ private Cap(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+ /**
+ * The Join specifies the treatment where lines and curve segments
+ * join on a stroked path. The default is MITER.
+ */
+ public enum Join {
+ /**
+ * The outer edges of a join meet at a sharp angle
+ */
+ MITER (0),
+ /**
+ * The outer edges of a join meet in a circular arc.
+ */
+ ROUND (1),
+ /**
+ * The outer edges of a join meet with a straight line
+ */
+ BEVEL (2);
+ private Join(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+ /**
+ * Align specifies how drawText aligns its text relative to the
+ * [x,y] coordinates. The default is LEFT.
+ */
+ public enum Align {
+ /**
+ * The text is drawn to the right of the x,y origin
+ */
+ LEFT (0),
+ /**
+ * The text is drawn centered horizontally on the x,y origin
+ */
+ CENTER (1),
+ /**
+ * The text is drawn to the left of the x,y origin
+ */
+ RIGHT (2);
+ private Align(int nativeInt) {
+ this.nativeInt = nativeInt;
+ }
+ final int nativeInt;
+ }
+
+
+ /**
+ * Class that describes the various metrics for a font at a given text size.
+ * Remember, Y values increase going down, so those values will be positive,
+ * and values that measure distances going up will be negative. This class
+ * is returned by getFontMetrics().
+ */
+ public static class FontMetrics {
+ /**
+ * The maximum distance above the baseline for the tallest glyph in
+ * the font at a given text size.
+ */
+ public float top;
+ /**
+ * The recommended distance above the baseline for singled spaced text.
+ */
+ public float ascent;
+ /**
+ * The recommended distance below the baseline for singled spaced text.
+ */
+ public float descent;
+ /**
+ * The maximum distance below the baseline for the lowest glyph in
+ * the font at a given text size.
+ */
+ public float bottom;
+ /**
+ * The recommended additional space to add between lines of text.
+ */
+ public float leading;
+ }
+
+ public org.jetbrains.skia.Paint thePaint;
+
+ private Typeface typeface;
+ private float textSize;
+
+ public Paint() {
+ thePaint = new org.jetbrains.skia.Paint();
+ }
+
+ public Paint(Paint paint) {
+ thePaint = paint.thePaint;
+
+ typeface = paint.typeface;
+ textSize = paint.textSize;
+ }
+
+ public Paint setColor(int color) {
+ thePaint.setColor(color);
+ return this;
+ }
+
+ public void setAntiAlias(boolean value) {
+ thePaint.setAntiAlias(value);
+ }
+
+ public Paint setStyle(Style style) {
+ // Map Style to Skiko Mode enum
+ org.jetbrains.skia.PaintMode mode = null;
+
+ switch(style) {
+ case FILL:
+ mode = org.jetbrains.skia.PaintMode.FILL;
+ break;
+ case STROKE:
+ mode = org.jetbrains.skia.PaintMode.STROKE;
+ break;
+ case FILL_AND_STROKE:
+ mode = org.jetbrains.skia.PaintMode.STROKE_AND_FILL;
+ break;
+ }
+
+ thePaint.setMode(mode);
+
+ return this;
+ }
+
+ public Paint setTypeface(Typeface typeface) {
+ this.typeface = typeface;
+ return this;
+ }
+
+ public Paint setTextSize(float v) {
+ textSize = v;
+ return this;
+ }
+
+ public void setARGB(int a, int r, int g, int b) {
+ thePaint.setColor(Color.argb(a, r, g, b));
+ }
+
+ public void setAlpha(int alpha) {
+ thePaint.setAlpha(alpha);
+ }
+
+ public void setStrokeJoin(Join join) {
+ PaintStrokeJoin strokeJoin = null;
+
+ // conversion
+ switch(join) {
+ case MITER:
+ strokeJoin = PaintStrokeJoin.MITER;
+ break;
+ case ROUND:
+ strokeJoin = PaintStrokeJoin.ROUND;
+ break;
+ case BEVEL:
+ strokeJoin = PaintStrokeJoin.BEVEL;
+ break;
+ }
+
+ thePaint.setStrokeJoin(strokeJoin);
+ }
+
+ public void setStrokeCap(Cap cap) {
+ PaintStrokeCap strokeCap = null;
+
+ // conversion
+ switch(cap) {
+ case BUTT:
+ strokeCap = PaintStrokeCap.BUTT;
+ break;
+ case ROUND:
+ strokeCap = PaintStrokeCap.ROUND;
+ break;
+ case SQUARE:
+ strokeCap = PaintStrokeCap.SQUARE;
+ break;
+ }
+
+ thePaint.setStrokeCap(strokeCap);
+ }
+
+ public void setStrokeWidth(float width) {
+ thePaint.setStrokeWidth(width);
+ }
+
+ public void setStrokeMiter(float miter) {
+ thePaint.setStrokeMiter(miter);
+ }
+
+ public void set(Paint src) {
+ thePaint = src.thePaint.makeClone();
+ typeface = src.typeface;
+ textSize = src.textSize;
+ }
+
+ public void reset() {
+ thePaint = new org.jetbrains.skia.Paint();
+ typeface = null;
+ textSize = 0;
+ }
+
+ public boolean hasGlyph(String text) {
+ return typeface.theTypeface.getStringGlyphs(text).length != 0;
+ }
+
+ // write getters here
+ public int getColor() {
+ return thePaint.getColor();
+ }
+
+ public boolean isAntiAlias() {
+ return thePaint.isAntiAlias();
+ }
+
+ public Style getStyle() {
+ switch(thePaint.getMode()) {
+ case FILL:
+ return Style.FILL;
+ case STROKE:
+ return Style.STROKE;
+ default:
+ return Style.FILL_AND_STROKE;
+ }
+ }
+
+ public float getStrokeWidth() {
+ return thePaint.getStrokeWidth();
+ }
+
+ public Cap getStrokeCap() {
+ switch(thePaint.getStrokeCap()) {
+ case ROUND:
+ return Cap.ROUND;
+ case SQUARE:
+ return Cap.SQUARE;
+ default:
+ return Cap.BUTT;
+ }
+ }
+
+ public Join getStrokeJoin() {
+ switch(thePaint.getStrokeJoin()) {
+ case ROUND:
+ return Join.ROUND;
+ case BEVEL:
+ return Join.BEVEL;
+ default:
+ return Join.MITER;
+ }
+ }
+
+ public float getStrokeMiter() {
+ return thePaint.getStrokeMiter();
+ }
+
+ public Typeface getTypeface() {
+ if(typeface == null) {
+ typeface = Typeface.DEFAULT;
+ }
+
+ return typeface;
+ }
+
+ private Font getFont() {
+ return FontCache.makeFont(getTypeface(), getTextSize());
+ }
+
+ public FontMetrics getFontMetrics() {
+ FontMetrics metrics = new FontMetrics();
+ org.jetbrains.skia.FontMetrics fontMetrics = getFont().getMetrics();
+
+ metrics.top = fontMetrics.getTop();
+ metrics.ascent = fontMetrics.getAscent();
+ metrics.descent = fontMetrics.getDescent();
+ metrics.bottom = fontMetrics.getBottom();
+ metrics.leading = fontMetrics.getLeading();
+
+ return metrics;
+ }
+
+ public float getTextSize() {
+ return textSize;
+ }
+
+}
diff --git a/Vision/src/main/java/android/graphics/Path.java b/Vision/src/main/java/android/graphics/Path.java
new file mode 100644
index 00000000..99abbcdd
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Path.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package android.graphics;
+
+import org.jetbrains.skia.PathDirection;
+import org.jetbrains.skia.RRect;
+import org.jetbrains.skia.Rect;
+
+public class Path {
+
+ /*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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.
+ */
+
+ /**
+ * Specifies how closed shapes (e.g. rects, ovals) are oriented when they
+ * are added to a path.
+ */
+ public enum Direction {
+ /** clockwise */
+ CW (0), // must match enum in SkPath.h
+ /** counter-clockwise */
+ CCW (1); // must match enum in SkPath.h
+ Direction(int ni) {
+ nativeInt = ni;
+ }
+ final int nativeInt;
+ }
+
+ public org.jetbrains.skia.Path thePath;
+
+ public Path() {
+ thePath = new org.jetbrains.skia.Path();
+ }
+
+ public Path(Path src) {
+ this();
+ thePath = src.thePath;
+ }
+
+ public void set(Path src) {
+ thePath = src.thePath;
+ }
+
+ public void addCircle(float x, float y, float radius, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addCircle(x, y, radius, skDir);
+ }
+
+ public void addRect(float left, float top, float right, float bottom, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addRect(new Rect(left, top, right, bottom), skDir, 0);
+ }
+
+ public void addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addRRect(RRect.makeLTRB(left, top, right, bottom, rx, ry), skDir, 0);
+ }
+
+ public void addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addRRect(new RRect(left, top, right, bottom, radii), skDir, 0);
+ }
+
+ public void addRoundRect(Rect rect, float[] radii, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addRRect(new RRect(rect.getLeft(), rect.getTop(), rect.getRight(), rect.getBottom(), radii), skDir, 0);
+ }
+
+ public void addRoundRect(Rect rect, float rx, float ry, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addRRect(RRect.makeLTRB(rect.getLeft(), rect.getTop(), rect.getRight(), rect.getBottom(), rx, ry), skDir, 0);
+ }
+
+ public void addOval(float left, float top, float right, float bottom, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addOval(new Rect(left, top, right, bottom), skDir, 0);
+ }
+
+ public void addOval(Rect oval, Direction dir) {
+ // map to skia direction
+ PathDirection skDir = null;
+ switch (dir) {
+ case CW:
+ skDir = PathDirection.CLOCKWISE;
+ break;
+ case CCW:
+ skDir = PathDirection.COUNTER_CLOCKWISE;
+ break;
+ }
+
+ thePath.addOval(oval, skDir, 0);
+ }
+
+ public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) {
+ thePath.addArc(new Rect(left, top, right, bottom), startAngle, sweepAngle);
+ }
+
+ public void addArc(Rect oval, float startAngle, float sweepAngle) {
+ thePath.addArc(oval, startAngle, sweepAngle);
+ }
+
+ public void moveTo(float x, float y) {
+ thePath.moveTo(x, y);
+ }
+
+ public void lineTo(float x, float y) {
+ thePath.lineTo(x, y);
+ }
+
+ public void quadTo(float x1, float y1, float x2, float y2) {
+ thePath.quadTo(x1, y1, x2, y2);
+ }
+
+ public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
+ thePath.cubicTo(x1, y1, x2, y2, x3, y3);
+ }
+
+ public void close() {
+ thePath.close();
+ }
+
+ public void reset() {
+ thePath.reset();
+ }
+
+ public void addPath(Path path) {
+ thePath.addPath(path.thePath, false);
+ }
+
+}
diff --git a/Vision/src/main/java/android/graphics/Rect.java b/Vision/src/main/java/android/graphics/Rect.java
new file mode 100644
index 00000000..97c1aa4e
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Rect.java
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * 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 android.graphics;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.awt.*;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+/**
+ * Rect holds four integer coordinates for a rectangle. The rectangle is
+ * represented by the coordinates of its 4 edges (left, top, right bottom).
+ * These fields can be accessed directly. Use width() and height() to retrieve
+ * the rectangle's width and height. Note: most methods do not check to see that
+ * the coordinates are sorted correctly (i.e. left <= right and top <= bottom).
+ *
+ * Note that the right and bottom coordinates are exclusive. This means a Rect
+ * being drawn untransformed onto a {@link android.graphics.Canvas} will draw
+ * into the column and row described by its left and top coordinates, but not
+ * those of its bottom and right.
+ */
+public final class Rect {
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
+ /**
+ * A helper class for flattened rectange pattern recognition. A separate
+ * class to avoid an initialization dependency on a regular expression
+ * causing Rect to not be initializable with an ahead-of-time compilation
+ * scheme.
+ */
+ private static final class UnflattenHelper {
+ private static final Pattern FLATTENED_PATTERN = Pattern.compile(
+ "(-?\\d+) (-?\\d+) (-?\\d+) (-?\\d+)");
+ static Matcher getMatcher(String str) {
+ return FLATTENED_PATTERN.matcher(str);
+ }
+ }
+ /**
+ * Create a new empty Rect. All coordinates are initialized to 0.
+ */
+ public Rect() {}
+ /**
+ * Create a new rectangle with the specified coordinates. Note: no range
+ * checking is performed, so the caller must ensure that left <= right and
+ * top <= bottom.
+ *
+ * @param left The X coordinate of the left side of the rectangle
+ * @param top The Y coordinate of the top of the rectangle
+ * @param right The X coordinate of the right side of the rectangle
+ * @param bottom The Y coordinate of the bottom of the rectangle
+ */
+ public Rect(int left, int top, int right, int bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+ /**
+ * Create a new rectangle, initialized with the values in the specified
+ * rectangle (which is left unmodified).
+ *
+ * @param r The rectangle whose coordinates are copied into the new
+ * rectangle.
+ */
+ public Rect(@Nullable Rect r) {
+ if (r == null) {
+ left = top = right = bottom = 0;
+ } else {
+ left = r.left;
+ top = r.top;
+ right = r.right;
+ bottom = r.bottom;
+ }
+ }
+ /**
+ * @hide
+ */
+ public Rect(@Nullable Insets r) {
+ if (r == null) {
+ left = top = right = bottom = 0;
+ } else {
+ left = r.left;
+ top = r.top;
+ right = r.right;
+ bottom = r.bottom;
+ }
+ }
+ /**
+ * Returns a copy of {@code r} if {@code r} is not {@code null}, or {@code null} otherwise.
+ *
+ * @hide
+ */
+ @Nullable
+ public static Rect copyOrNull(@Nullable Rect r) {
+ return r == null ? null : new Rect(r);
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Rect r = (Rect) o;
+ return left == r.left && top == r.top && right == r.right && bottom == r.bottom;
+ }
+ @Override
+ public int hashCode() {
+ int result = left;
+ result = 31 * result + top;
+ result = 31 * result + right;
+ result = 31 * result + bottom;
+ return result;
+ }
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(32);
+ sb.append("Rect("); sb.append(left); sb.append(", ");
+ sb.append(top); sb.append(" - "); sb.append(right);
+ sb.append(", "); sb.append(bottom); sb.append(")");
+ return sb.toString();
+ }
+ /**
+ * Return a string representation of the rectangle in a compact form.
+ */
+ @NonNull
+ public String toShortString() {
+ return toShortString(new StringBuilder(32));
+ }
+
+ /**
+ * Return a string representation of the rectangle in a compact form.
+ * @hide
+ */
+ @NonNull
+ public String toShortString(@NonNull StringBuilder sb) {
+ sb.setLength(0);
+ sb.append('['); sb.append(left); sb.append(',');
+ sb.append(top); sb.append("]["); sb.append(right);
+ sb.append(','); sb.append(bottom); sb.append(']');
+ return sb.toString();
+ }
+ /**
+ * Return a string representation of the rectangle in a well-defined format.
+ *
+ * @return Returns a new String of the form "left top right bottom"
+ */
+ @NonNull
+ public String flattenToString() {
+ StringBuilder sb = new StringBuilder(32);
+ // WARNING: Do not change the format of this string, it must be
+ // preserved because Rects are saved in this flattened format.
+ sb.append(left);
+ sb.append(' ');
+ sb.append(top);
+ sb.append(' ');
+ sb.append(right);
+ sb.append(' ');
+ sb.append(bottom);
+ return sb.toString();
+ }
+
+ /**
+ * Print short representation to given writer.
+ * @hide
+ */
+ public void printShortString(@NonNull PrintWriter pw) {
+ pw.print('['); pw.print(left); pw.print(',');
+ pw.print(top); pw.print("]["); pw.print(right);
+ pw.print(','); pw.print(bottom); pw.print(']');
+ }
+ /**
+ * Returns true if the rectangle is empty (left >= right or top >= bottom)
+ */
+ public final boolean isEmpty() {
+ return left >= right || top >= bottom;
+ }
+ /**
+ * @return {@code true} if the rectangle is valid (left <= right and top <= bottom).
+ * @hide
+ */
+ public boolean isValid() {
+ return left <= right && top <= bottom;
+ }
+ /**
+ * @return the rectangle's width. This does not check for a valid rectangle
+ * (i.e. left <= right) so the result may be negative.
+ */
+ public final int width() {
+ return right - left;
+ }
+ /**
+ * @return the rectangle's height. This does not check for a valid rectangle
+ * (i.e. top <= bottom) so the result may be negative.
+ */
+ public final int height() {
+ return bottom - top;
+ }
+
+ /**
+ * @return the horizontal center of the rectangle. If the computed value
+ * is fractional, this method returns the largest integer that is
+ * less than the computed value.
+ */
+ public final int centerX() {
+ return (left + right) >> 1;
+ }
+
+ /**
+ * @return the vertical center of the rectangle. If the computed value
+ * is fractional, this method returns the largest integer that is
+ * less than the computed value.
+ */
+ public final int centerY() {
+ return (top + bottom) >> 1;
+ }
+
+ /**
+ * @return the exact horizontal center of the rectangle as a float.
+ */
+ public final float exactCenterX() {
+ return (left + right) * 0.5f;
+ }
+
+ /**
+ * @return the exact vertical center of the rectangle as a float.
+ */
+ public final float exactCenterY() {
+ return (top + bottom) * 0.5f;
+ }
+ /**
+ * Set the rectangle to (0,0,0,0)
+ */
+ public void setEmpty() {
+ left = right = top = bottom = 0;
+ }
+ /**
+ * Set the rectangle's coordinates to the specified values. Note: no range
+ * checking is performed, so it is up to the caller to ensure that
+ * left <= right and top <= bottom.
+ *
+ * @param left The X coordinate of the left side of the rectangle
+ * @param top The Y coordinate of the top of the rectangle
+ * @param right The X coordinate of the right side of the rectangle
+ * @param bottom The Y coordinate of the bottom of the rectangle
+ */
+ public void set(int left, int top, int right, int bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+ /**
+ * Copy the coordinates from src into this rectangle.
+ *
+ * @param src The rectangle whose coordinates are copied into this
+ * rectangle.
+ */
+ public void set(@NonNull Rect src) {
+ this.left = src.left;
+ this.top = src.top;
+ this.right = src.right;
+ this.bottom = src.bottom;
+ }
+ /**
+ * Offset the rectangle by adding dx to its left and right coordinates, and
+ * adding dy to its top and bottom coordinates.
+ *
+ * @param dx The amount to add to the rectangle's left and right coordinates
+ * @param dy The amount to add to the rectangle's top and bottom coordinates
+ */
+ public void offset(int dx, int dy) {
+ left += dx;
+ top += dy;
+ right += dx;
+ bottom += dy;
+ }
+ /**
+ * Offset the rectangle to a specific (left, top) position,
+ * keeping its width and height the same.
+ *
+ * @param newLeft The new "left" coordinate for the rectangle
+ * @param newTop The new "top" coordinate for the rectangle
+ */
+ public void offsetTo(int newLeft, int newTop) {
+ right += newLeft - left;
+ bottom += newTop - top;
+ left = newLeft;
+ top = newTop;
+ }
+ /**
+ * Inset the rectangle by (dx,dy). If dx is positive, then the sides are
+ * moved inwards, making the rectangle narrower. If dx is negative, then the
+ * sides are moved outwards, making the rectangle wider. The same holds true
+ * for dy and the top and bottom.
+ *
+ * @param dx The amount to add(subtract) from the rectangle's left(right)
+ * @param dy The amount to add(subtract) from the rectangle's top(bottom)
+ */
+ public void inset(int dx, int dy) {
+ left += dx;
+ top += dy;
+ right -= dx;
+ bottom -= dy;
+ }
+ /**
+ * Insets the rectangle on all sides specified by the dimensions of the {@code insets}
+ * rectangle.
+ * @hide
+ * @param insets The rectangle specifying the insets on all side.
+ */
+ public void inset(@NonNull Rect insets) {
+ left += insets.left;
+ top += insets.top;
+ right -= insets.right;
+ bottom -= insets.bottom;
+ }
+ /**
+ * Insets the rectangle on all sides specified by the dimensions of {@code insets}.
+ *
+ * @param insets The insets to inset the rect by.
+ */
+ public void inset(@NonNull Insets insets) {
+ left += insets.left;
+ top += insets.top;
+ right -= insets.right;
+ bottom -= insets.bottom;
+ }
+ /**
+ * Insets the rectangle on all sides specified by the insets.
+ *
+ * @param left The amount to add from the rectangle's left
+ * @param top The amount to add from the rectangle's top
+ * @param right The amount to subtract from the rectangle's right
+ * @param bottom The amount to subtract from the rectangle's bottom
+ */
+ public void inset(int left, int top, int right, int bottom) {
+ this.left += left;
+ this.top += top;
+ this.right -= right;
+ this.bottom -= bottom;
+ }
+ /**
+ * Returns true if (x,y) is inside the rectangle. The left and top are
+ * considered to be inside, while the right and bottom are not. This means
+ * that for a x,y to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ *
+ * @param x The X coordinate of the point being tested for containment
+ * @param y The Y coordinate of the point being tested for containment
+ * @return true iff (x,y) are contained by the rectangle, where containment
+ * means left <= x < right and top <= y < bottom
+ */
+ public boolean contains(int x, int y) {
+ return left < right && top < bottom // check for empty first
+ && x >= left && x < right && y >= top && y < bottom;
+ }
+ /**
+ * Returns true iff the 4 specified sides of a rectangle are inside or equal
+ * to this rectangle. i.e. is this rectangle a superset of the specified
+ * rectangle. An empty rectangle never contains another rectangle.
+ *
+ * @param left The left side of the rectangle being tested for containment
+ * @param top The top of the rectangle being tested for containment
+ * @param right The right side of the rectangle being tested for containment
+ * @param bottom The bottom of the rectangle being tested for containment
+ * @return true iff the the 4 specified sides of a rectangle are inside or
+ * equal to this rectangle
+ */
+ public boolean contains(int left, int top, int right, int bottom) {
+ // check for empty first
+ return this.left < this.right && this.top < this.bottom
+ // now check for containment
+ && this.left <= left && this.top <= top
+ && this.right >= right && this.bottom >= bottom;
+ }
+ /**
+ * Returns true iff the specified rectangle r is inside or equal to this
+ * rectangle. An empty rectangle never contains another rectangle.
+ *
+ * @param r The rectangle being tested for containment.
+ * @return true iff the specified rectangle r is inside or equal to this
+ * rectangle
+ */
+ public boolean contains(@NonNull Rect r) {
+ // check for empty first
+ return this.left < this.right && this.top < this.bottom
+ // now check for containment
+ && left <= r.left && top <= r.top && right >= r.right && bottom >= r.bottom;
+ }
+ /**
+ * If the rectangle specified by left,top,right,bottom intersects this
+ * rectangle, return true and set this rectangle to that intersection,
+ * otherwise return false and do not change this rectangle. No check is
+ * performed to see if either rectangle is empty. Note: To just test for
+ * intersection, use {@link #intersects(Rect, Rect)}.
+ *
+ * @param left The left side of the rectangle being intersected with this
+ * rectangle
+ * @param top The top of the rectangle being intersected with this rectangle
+ * @param right The right side of the rectangle being intersected with this
+ * rectangle.
+ * @param bottom The bottom of the rectangle being intersected with this
+ * rectangle.
+ * @return true if the specified rectangle and this rectangle intersect
+ * (and this rectangle is then set to that intersection) else
+ * return false and do not change this rectangle.
+ */
+ public boolean intersect(int left, int top, int right, int bottom) {
+ if (this.left < right && left < this.right && this.top < bottom && top < this.bottom) {
+ if (this.left < left) this.left = left;
+ if (this.top < top) this.top = top;
+ if (this.right > right) this.right = right;
+ if (this.bottom > bottom) this.bottom = bottom;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If the specified rectangle intersects this rectangle, return true and set
+ * this rectangle to that intersection, otherwise return false and do not
+ * change this rectangle. No check is performed to see if either rectangle
+ * is empty. To just test for intersection, use intersects()
+ *
+ * @param r The rectangle being intersected with this rectangle.
+ * @return true if the specified rectangle and this rectangle intersect
+ * (and this rectangle is then set to that intersection) else
+ * return false and do not change this rectangle.
+ */
+ public boolean intersect(@NonNull Rect r) {
+ return intersect(r.left, r.top, r.right, r.bottom);
+ }
+ /**
+ * If the specified rectangle intersects this rectangle, set this rectangle to that
+ * intersection, otherwise set this rectangle to the empty rectangle.
+ * @see #inset(int, int, int, int) but without checking if the rects overlap.
+ * @hide
+ */
+ public void intersectUnchecked(@NonNull Rect other) {
+ left = Math.max(left, other.left);
+ top = Math.max(top, other.top);
+ right = Math.min(right, other.right);
+ bottom = Math.min(bottom, other.bottom);
+ }
+ /**
+ * If rectangles a and b intersect, return true and set this rectangle to
+ * that intersection, otherwise return false and do not change this
+ * rectangle. No check is performed to see if either rectangle is empty.
+ * To just test for intersection, use intersects()
+ *
+ * @param a The first rectangle being intersected with
+ * @param b The second rectangle being intersected with
+ * @return true iff the two specified rectangles intersect. If they do, set
+ * this rectangle to that intersection. If they do not, return
+ * false and do not change this rectangle.
+ */
+ public boolean setIntersect(@NonNull Rect a, @NonNull Rect b) {
+ if (a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom) {
+ left = Math.max(a.left, b.left);
+ top = Math.max(a.top, b.top);
+ right = Math.min(a.right, b.right);
+ bottom = Math.min(a.bottom, b.bottom);
+ return true;
+ }
+ return false;
+ }
+ /**
+ * Returns true if this rectangle intersects the specified rectangle.
+ * In no event is this rectangle modified. No check is performed to see
+ * if either rectangle is empty. To record the intersection, use intersect()
+ * or setIntersect().
+ *
+ * @param left The left side of the rectangle being tested for intersection
+ * @param top The top of the rectangle being tested for intersection
+ * @param right The right side of the rectangle being tested for
+ * intersection
+ * @param bottom The bottom of the rectangle being tested for intersection
+ * @return true iff the specified rectangle intersects this rectangle. In
+ * no event is this rectangle modified.
+ */
+ public boolean intersects(int left, int top, int right, int bottom) {
+ return this.left < right && left < this.right && this.top < bottom && top < this.bottom;
+ }
+ /**
+ * Returns true iff the two specified rectangles intersect. In no event are
+ * either of the rectangles modified. To record the intersection,
+ * use {@link #intersect(Rect)} or {@link #setIntersect(Rect, Rect)}.
+ *
+ * @param a The first rectangle being tested for intersection
+ * @param b The second rectangle being tested for intersection
+ * @return true iff the two specified rectangles intersect. In no event are
+ * either of the rectangles modified.
+ */
+ public static boolean intersects(@NonNull Rect a, @NonNull Rect b) {
+ return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
+ }
+ /**
+ * Update this Rect to enclose itself and the specified rectangle. If the
+ * specified rectangle is empty, nothing is done. If this rectangle is empty
+ * it is set to the specified rectangle.
+ *
+ * @param left The left edge being unioned with this rectangle
+ * @param top The top edge being unioned with this rectangle
+ * @param right The right edge being unioned with this rectangle
+ * @param bottom The bottom edge being unioned with this rectangle
+ */
+ public void union(int left, int top, int right, int bottom) {
+ if ((left < right) && (top < bottom)) {
+ if ((this.left < this.right) && (this.top < this.bottom)) {
+ if (this.left > left) this.left = left;
+ if (this.top > top) this.top = top;
+ if (this.right < right) this.right = right;
+ if (this.bottom < bottom) this.bottom = bottom;
+ } else {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+ }
+ }
+ /**
+ * Update this Rect to enclose itself and the specified rectangle. If the
+ * specified rectangle is empty, nothing is done. If this rectangle is empty
+ * it is set to the specified rectangle.
+ *
+ * @param r The rectangle being unioned with this rectangle
+ */
+ public void union(@NonNull Rect r) {
+ union(r.left, r.top, r.right, r.bottom);
+ }
+
+ /**
+ * Update this Rect to enclose itself and the [x,y] coordinate. There is no
+ * check to see that this rectangle is non-empty.
+ *
+ * @param x The x coordinate of the point to add to the rectangle
+ * @param y The y coordinate of the point to add to the rectangle
+ */
+ public void union(int x, int y) {
+ if (x < left) {
+ left = x;
+ } else if (x > right) {
+ right = x;
+ }
+ if (y < top) {
+ top = y;
+ } else if (y > bottom) {
+ bottom = y;
+ }
+ }
+ /**
+ * Swap top/bottom or left/right if there are flipped (i.e. left > right
+ * and/or top > bottom). This can be called if
+ * the edges are computed separately, and may have crossed over each other.
+ * If the edges are already correct (i.e. left <= right and top <= bottom)
+ * then nothing is done.
+ */
+ public void sort() {
+ if (left > right) {
+ int temp = left;
+ left = right;
+ right = temp;
+ }
+ if (top > bottom) {
+ int temp = top;
+ top = bottom;
+ bottom = temp;
+ }
+ }
+ /**
+ * Splits this Rect into small rects of the same width.
+ * @hide
+ */
+ public void splitVertically(@NonNull Rect ...splits) {
+ final int count = splits.length;
+ final int splitWidth = width() / count;
+ for (int i = 0; i < count; i++) {
+ final Rect split = splits[i];
+ split.left = left + (splitWidth * i);
+ split.top = top;
+ split.right = split.left + splitWidth;
+ split.bottom = bottom;
+ }
+ }
+ /**
+ * Splits this Rect into small rects of the same height.
+ * @hide
+ */
+ public void splitHorizontally(@NonNull Rect ...outSplits) {
+ final int count = outSplits.length;
+ final int splitHeight = height() / count;
+ for (int i = 0; i < count; i++) {
+ final Rect split = outSplits[i];
+ split.left = left;
+ split.top = top + (splitHeight * i);
+ split.right = right;
+ split.bottom = split.top + splitHeight;
+ }
+ }
+
+ public org.jetbrains.skia.Rect toSkijaRect() {
+ return new org.jetbrains.skia.Rect(left, top, right, bottom);
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/android/graphics/Typeface.java b/Vision/src/main/java/android/graphics/Typeface.java
new file mode 100644
index 00000000..b6967f6e
--- /dev/null
+++ b/Vision/src/main/java/android/graphics/Typeface.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package android.graphics;
+
+import org.jetbrains.skia.FontMgr;
+import org.jetbrains.skia.FontStyle;
+
+public class Typeface {
+
+ public static Typeface DEFAULT = new Typeface(FontMgr.Companion.getDefault().matchFamilyStyle(null, FontStyle.Companion.getNORMAL()));
+ public static Typeface DEFAULT_BOLD = new Typeface(FontMgr.Companion.getDefault().matchFamilyStyle(null, FontStyle.Companion.getBOLD()));
+ public static Typeface DEFAULT_ITALIC = new Typeface(FontMgr.Companion.getDefault().matchFamilyStyle(null, FontStyle.Companion.getITALIC()));
+
+ public org.jetbrains.skia.Typeface theTypeface;
+
+ public Typeface(long ptr) {
+ theTypeface = new org.jetbrains.skia.Typeface(ptr);
+ }
+
+ private Typeface(org.jetbrains.skia.Typeface typeface) {
+ theTypeface = typeface;
+ }
+
+ public Rect getBounds() {
+ return new Rect(
+ (int) theTypeface.getBounds().getLeft(),
+ (int) theTypeface.getBounds().getTop(),
+ (int) theTypeface.getBounds().getRight(),
+ (int) theTypeface.getBounds().getBottom()
+ );
+ }
+
+}
diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java
new file mode 100644
index 00000000..18d101c4
--- /dev/null
+++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/LinearOpMode.java
@@ -0,0 +1,234 @@
+/* Copyright (c) 2014 Qualcomm Technologies Inc
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Qualcomm Technologies Inc nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+package com.qualcomm.robotcore.eventloop.opmode;
+
+import io.github.deltacv.vision.external.source.ThreadSourceHander;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class LinearOpMode extends OpMode {
+ protected final Object lock = new Object();
+ private LinearOpModeHelperThread helper = new LinearOpModeHelperThread(this);
+ private RuntimeException catchedException = null;
+
+ public LinearOpMode() {
+ }
+
+ //------------------------------------------------------------------------------------------------
+ // Operations
+ //------------------------------------------------------------------------------------------------
+
+ /**
+ * Override this method and place your code here.
+ *
+ * Please do not swallow the InterruptedException, as it is used in cases
+ * where the op mode needs to be terminated early.
+ * @throws InterruptedException
+ */
+ abstract public void runOpMode() throws InterruptedException;
+
+ /**
+ * Pauses the Linear Op Mode until start has been pressed or until the current thread
+ * is interrupted.
+ */
+ public void waitForStart() {
+ while (!isStarted() && !Thread.currentThread().isInterrupted()) { idle(); }
+ }
+
+ /**
+ * Puts the current thread to sleep for a bit as it has nothing better to do. This allows other
+ * threads in the system to run.
+ *
+ *
One can use this method when you have nothing better to do in your code as you await state
+ * managed by other threads to change. Calling idle() is entirely optional: it just helps make
+ * the system a little more responsive and a little more efficient.
+ *
+ * @see #opModeIsActive()
+ */
+ public final void idle() {
+ // Otherwise, yield back our thread scheduling quantum and give other threads at
+ // our priority level a chance to run
+ Thread.yield();
+ }
+
+ /**
+ * Sleeps for the given amount of milliseconds, or until the thread is interrupted. This is
+ * simple shorthand for the operating-system-provided {@link Thread#sleep(long) sleep()} method.
+ *
+ * @param milliseconds amount of time to sleep, in milliseconds
+ * @see Thread#sleep(long)
+ */
+ public final void sleep(long milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Answer as to whether this opMode is active and the robot should continue onwards. If the
+ * opMode is not active, the OpMode should terminate at its earliest convenience.
+ *
+ * Note that internally this method calls {@link #idle()}
+ *
+ * @return whether the OpMode is currently active. If this returns false, you should
+ * break out of the loop in your {@link #runOpMode()} method and return to its caller.
+ * @see #runOpMode()
+ * @see #isStarted()
+ * @see #isStopRequested()
+ */
+ public final boolean opModeIsActive() {
+ boolean isActive = !this.isStopRequested() && this.isStarted();
+ if (isActive) {
+ idle();
+ }
+ return isActive;
+ }
+
+ /**
+ * Can be used to break out of an Init loop when false is returned. Touching
+ * Start or Stop will return false.
+ *
+ * @return Whether the OpMode is currently in Init. A return of false can exit
+ * an Init loop and proceed with the next action.
+ */
+ public final boolean opModeInInit() {
+ return !isStarted() && !isStopRequested();
+ }
+
+ /**
+ * Has the opMode been started?
+ *
+ * @return whether this opMode has been started or not
+ * @see #opModeIsActive()
+ * @see #isStopRequested()
+ */
+ public final boolean isStarted() {
+ return this.isStarted || Thread.currentThread().isInterrupted();
+ }
+
+ /**
+ * Has the the stopping of the opMode been requested?
+ *
+ * @return whether stopping opMode has been requested or not
+ * @see #opModeIsActive()
+ * @see #isStarted()
+ */
+ public final boolean isStopRequested() {
+ return this.stopRequested || Thread.currentThread().isInterrupted();
+ }
+
+
+ //------------------------------------------------------------------------------------------------
+ // OpMode inheritance
+ //------------------------------------------------------------------------------------------------
+
+ @Override
+ public final void init() {
+ isStarted = false;
+ stopRequested = false;
+
+ ThreadSourceHander.register(helper, ThreadSourceHander.threadHander());
+
+ helper.start();
+ }
+
+ @Override
+ public final void init_loop() { }
+
+ @Override
+ public final void start() {
+ stopRequested = false;
+ isStarted = true;
+ }
+
+ @Override
+ public final void loop() {
+ synchronized (lock) {
+ if (catchedException != null) {
+ throw catchedException;
+ }
+ }
+ }
+
+ @Override
+ public final void stop() {
+ /*
+ * Get out of dodge. Been here, done this.
+ */
+ if(stopRequested) { return; }
+
+ stopRequested = true;
+
+ helper.interrupt();
+
+ try {
+ helper.join();
+ } catch (InterruptedException ignored) {
+ }
+ }
+
+ private static class LinearOpModeHelperThread extends Thread {
+
+ LinearOpMode opMode;
+
+ static Logger logger = LoggerFactory.getLogger(LinearOpModeHelperThread.class);
+
+ public LinearOpModeHelperThread(LinearOpMode opMode) {
+ super("Thread-LinearOpModeHelper-" + opMode.getClass().getSimpleName());
+
+ this.opMode = opMode;
+ }
+
+ @Override
+ public void run() {
+ logger.info("{}: starting", opMode.getClass().getSimpleName());
+
+ try {
+ opMode.runOpMode();
+ Thread.sleep(0);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.info("{}: interrupted", opMode.getClass().getSimpleName());
+ } catch (RuntimeException e) {
+ synchronized (opMode.lock) {
+ opMode.catchedException = e;
+ }
+ }
+
+ logger.info("{}: stopped", opMode.getClass().getSimpleName());
+ }
+
+ }
+
+}
diff --git a/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java
new file mode 100644
index 00000000..557d44fd
--- /dev/null
+++ b/Vision/src/main/java/com/qualcomm/robotcore/eventloop/opmode/OpMode.java
@@ -0,0 +1,197 @@
+/* Copyright (c) 2014 Qualcomm Technologies Inc
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Qualcomm Technologies Inc nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+package com.qualcomm.robotcore.eventloop.opmode;
+
+import com.qualcomm.robotcore.hardware.HardwareMap;
+import io.github.deltacv.vision.external.util.FrameQueue;
+import io.github.deltacv.vision.internal.opmode.OpModeNotification;
+import io.github.deltacv.vision.internal.opmode.OpModeNotifier;
+import io.github.deltacv.vision.internal.opmode.OpModeState;
+import org.openftc.easyopencv.TimestampedOpenCvPipeline;
+import org.firstinspires.ftc.robotcore.external.Telemetry;
+import org.opencv.core.Mat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class OpMode extends TimestampedOpenCvPipeline { // never in my life would i have imagined...
+
+ private Logger logger = LoggerFactory.getLogger(OpMode.class);
+
+ public Telemetry telemetry;
+
+ public OpModeNotifier notifier = new OpModeNotifier();
+
+ volatile boolean isStarted = false;
+ volatile boolean stopRequested = false;
+
+ protected FrameQueue inputQueue;
+
+ public HardwareMap hardwareMap;
+
+ public OpMode(int maxInputQueueCapacity) {
+ inputQueue = new FrameQueue(maxInputQueueCapacity);
+ }
+
+ public OpMode() {
+ this(10);
+ }
+
+ /* BEGIN OpMode abstract methods */
+
+ /**
+ * User defined init method
+ *
+ * This method will be called once when the INIT button is pressed.
+ */
+ abstract public void init();
+
+ /**
+ * User defined init_loop method
+ *
+ * This method will be called repeatedly when the INIT button is pressed.
+ * This method is optional. By default this method takes no action.
+ */
+ public void init_loop() {};
+
+ /**
+ * User defined start method.
+ *
+ * This method will be called once when the PLAY button is first pressed.
+ * This method is optional. By default this method takes not action.
+ * Example usage: Starting another thread.
+ *
+ */
+ public void start() {};
+
+ /**
+ * User defined loop method
+ *
+ * This method will be called repeatedly in a loop while this op mode is running
+ */
+ abstract public void loop();
+
+ /**
+ * User defined stop method
+ *
+ * This method will be called when this op mode is first disabled.
+ *
+ * The stop method is optional. By default this method takes no action.
+ */
+ public void stop() {}; // normally called by OpModePipelineHandler
+
+ public void requestOpModeStop() {
+ notifier.notify(OpModeNotification.STOP);
+ }
+
+ /* BEGIN OpenCvPipeline Impl */
+
+ private boolean stopped = false;
+
+ @Override
+ public final void init(Mat mat) {
+ if(stopped) {
+ throw new IllegalStateException("Trying to reuse already stopped OpMode");
+ }
+ }
+
+ @Override
+ public final Mat processFrame(Mat input, long captureTimeNanos) {
+ if(stopped) {
+ throw new IllegalStateException("Trying to reuse already stopped OpMode");
+ }
+
+ OpModeNotification notification = notifier.poll();
+
+ if(notification != OpModeNotification.NOTHING) {
+ logger.info("OpModeNotification: {}, OpModeState: {}", notification, notifier.getState());
+ }
+
+ switch(notification) {
+ case INIT:
+ if(notifier.getState() == OpModeState.START) break;
+
+ init();
+ notifier.notify(OpModeState.INIT);
+ break;
+ case START:
+ if(notifier.getState() == OpModeState.STOP || notifier.getState() == OpModeState.STOPPED) break;
+
+ start();
+ notifier.notify(OpModeState.START);
+ break;
+ case STOP:
+ forceStop();
+ break;
+ case NOTHING:
+ break;
+ }
+
+ OpModeState state = notifier.getState();
+
+ switch(state) {
+ case SELECTED:
+ case STOP:
+ case STOPPED:
+ break;
+ case INIT:
+ init_loop();
+
+ if(!(this instanceof LinearOpMode)) {
+ telemetry.update();
+ }
+ break;
+ case START:
+ loop();
+
+ if(!(this instanceof LinearOpMode)) {
+ telemetry.update();
+ }
+ break;
+ }
+
+ return null; // OpModes don't actually show anything to the viewport, we'll delegate that to OpenCvCamera-s
+ }
+
+ @Override
+ public final void onViewportTapped() {
+ }
+
+ public void forceStop() {
+ if(stopped) return;
+
+ notifier.notify(OpModeState.STOP);
+ stop();
+ notifier.notify(OpModeState.STOPPED);
+
+ stopped = true;
+ }
+}
diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java
new file mode 100644
index 00000000..19dee889
--- /dev/null
+++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareDevice.java
@@ -0,0 +1,83 @@
+/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Qualcomm Technologies Inc nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+package com.qualcomm.robotcore.hardware;
+
+/**
+ * Interface used by Hardware Devices
+ */
+public interface HardwareDevice {
+
+ enum Manufacturer {
+ Unknown, Other, Lego, HiTechnic, ModernRobotics, Adafruit, Matrix, Lynx, AMS, STMicroelectronics, Broadcom
+ }
+
+ /**
+ * Returns an indication of the manufacturer of this device.
+ * @return the device's manufacturer
+ */
+ Manufacturer getManufacturer();
+
+ /**
+ * Returns a string suitable for display to the user as to the type of device.
+ * Note that this is a device-type-specific name; it has nothing to do with the
+ * name by which a user might have configured the device in a robot configuration.
+ *
+ * @return device manufacturer and name
+ */
+ String getDeviceName();
+
+ /**
+ * Get connection information about this device in a human readable format
+ *
+ * @return connection info
+ */
+ String getConnectionInfo();
+
+ /**
+ * Version
+ *
+ * @return get the version of this device
+ */
+ int getVersion();
+
+ /**
+ * Resets the device's configuration to that which is expected at the beginning of an OpMode.
+ * For example, motors will reset the their direction to 'forward'.
+ */
+ void resetDeviceConfigurationForOpMode();
+
+ /**
+ * Closes this device
+ */
+ void close();
+
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java
new file mode 100644
index 00000000..f0c39b30
--- /dev/null
+++ b/Vision/src/main/java/com/qualcomm/robotcore/hardware/HardwareMap.java
@@ -0,0 +1,27 @@
+package com.qualcomm.robotcore.hardware;
+
+import io.github.deltacv.vision.external.source.ThreadSourceHander;
+import io.github.deltacv.vision.internal.source.ftc.SourcedCameraNameImpl;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName;
+
+public class HardwareMap {
+
+ private static boolean hasSuperclass(Class> clazz, Class> superClass) {
+ try {
+ clazz.asSubclass(superClass);
+ return true;
+ } catch (ClassCastException ex) {
+ return false;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public T get(Class classType, String deviceName) {
+ if(hasSuperclass(classType, CameraName.class)) {
+ return (T) new SourcedCameraNameImpl(ThreadSourceHander.hand(deviceName));
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt b/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt
new file mode 100644
index 00000000..ff0e6b60
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/PipelineRenderHook.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external
+
+import android.graphics.Canvas
+import org.openftc.easyopencv.OpenCvViewport
+import org.openftc.easyopencv.OpenCvViewport.RenderHook
+
+object PipelineRenderHook : RenderHook {
+ override fun onDrawFrame(canvas: Canvas, onscreenWidth: Int, onscreenHeight: Int, scaleBmpPxToCanvasPx: Float, canvasDensityScale: Float, userContext: Any) {
+ val frameContext = userContext as OpenCvViewport.FrameContext
+
+ // We must make sure that we call onDrawFrame() for the same pipeline that set the
+ // context object when requesting a draw hook. (i.e. we can't just call onDrawFrame()
+ // for whatever pipeline happens to be currently attached; it might have an entirely
+ // different notion of what to expect in the context object)
+ if (frameContext.generatingPipeline != null) {
+ frameContext.generatingPipeline.onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, canvasDensityScale, frameContext.userContext)
+ }
+ }
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java
new file mode 100644
index 00000000..cf01296c
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/SourcedOpenCvCamera.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2023 OpenFTC & EOCV-Sim implementation by Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external;
+
+import io.github.deltacv.vision.external.source.VisionSource;
+import io.github.deltacv.vision.external.source.VisionSourced;
+import org.opencv.core.Core;
+import org.opencv.core.Mat;
+import org.opencv.core.Size;
+import org.openftc.easyopencv.*;
+
+public class SourcedOpenCvCamera extends OpenCvCameraBase implements OpenCvWebcam, VisionSourced {
+
+ private final VisionSource source;
+ OpenCvViewport handedViewport;
+
+ boolean streaming = false;
+
+ public SourcedOpenCvCamera(VisionSource source, OpenCvViewport handedViewport, boolean viewportEnabled) {
+ super(handedViewport, viewportEnabled);
+
+ this.source = source;
+ this.handedViewport = handedViewport;
+ }
+
+ @Override
+ public int openCameraDevice() {
+ prepareForOpenCameraDevice();
+
+ return source.init();
+ }
+
+ @Override
+ public void openCameraDeviceAsync(AsyncCameraOpenListener cameraOpenListener) {
+ new Thread(() -> {
+ int code = openCameraDevice();
+
+ if(code == 0) {
+ cameraOpenListener.onOpened();
+ } else {
+ cameraOpenListener.onError(code);
+ }
+ }).start();
+ }
+
+ @Override
+ public void closeCameraDevice() {
+ synchronized (source) {
+ source.close();
+ }
+ }
+
+ @Override
+ public void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener) {
+ new Thread(() -> {
+ closeCameraDevice();
+ cameraCloseListener.onClose();
+ }).start();
+ }
+
+ @Override
+ public void startStreaming(int width, int height) {
+ startStreaming(width, height, getDefaultRotation());
+ }
+
+ @Override
+ public void startStreaming(int width, int height, OpenCvCameraRotation rotation) {
+ prepareForStartStreaming(width, height, rotation);
+
+ synchronized (source) {
+ source.start(new Size(width, height));
+ source.attach(this);
+ }
+
+ streaming = true;
+ }
+
+ @Override
+ public void stopStreaming() {
+ source.stop();
+ cleanupForEndStreaming();
+ }
+
+ @Override
+ protected OpenCvCameraRotation getDefaultRotation() {
+ return OpenCvCameraRotation.UPRIGHT;
+ }
+
+ @Override
+ protected int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation)
+ {
+ /*
+ * The camera sensor in a webcam is mounted in the logical manner, such
+ * that the raw image is upright when the webcam is used in its "normal"
+ * orientation. However, if the user is using it in any other orientation,
+ * we need to manually rotate the image.
+ */
+
+ if(rotation == OpenCvCameraRotation.SENSOR_NATIVE)
+ {
+ return -1;
+ }
+ else if(rotation == OpenCvCameraRotation.SIDEWAYS_LEFT)
+ {
+ return Core.ROTATE_90_COUNTERCLOCKWISE;
+ }
+ else if(rotation == OpenCvCameraRotation.SIDEWAYS_RIGHT)
+ {
+ return Core.ROTATE_90_CLOCKWISE;
+ }
+ else if(rotation == OpenCvCameraRotation.UPSIDE_DOWN)
+ {
+ return Core.ROTATE_180;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ @Override
+ protected boolean cameraOrientationIsTiedToDeviceOrientation() {
+ return false;
+ }
+
+ @Override
+ protected boolean isStreaming() {
+ return streaming;
+ }
+
+ @Override
+ public void onFrameStart() {
+ if(!isStreaming()) return;
+
+ notifyStartOfFrameProcessing();
+ }
+
+ //
+ // Inheritance from Sourced
+ //
+
+ @Override
+ public void onNewFrame(Mat frame, long timestamp) {
+ if(!isStreaming()) return;
+ if(frame == null || frame.empty()) return;
+
+ handleFrameUserCrashable(frame, timestamp);
+ }
+
+ @Override
+ public void stop() {
+ closeCameraDevice();
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SkiaPanel.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SkiaPanel.kt
new file mode 100644
index 00000000..16fd8991
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SkiaPanel.kt
@@ -0,0 +1,34 @@
+package io.github.deltacv.vision.external.gui
+
+import org.jetbrains.skiko.ClipComponent
+import org.jetbrains.skiko.SkiaLayer
+import java.awt.Color
+import java.awt.Component
+import javax.swing.JLayeredPane
+
+class SkiaPanel(private val layer: SkiaLayer) : JLayeredPane() {
+
+ init {
+ layout = null
+ background = Color.white
+ }
+
+ override fun add(component: Component): Component {
+ layer.clipComponents.add(ClipComponent(component))
+ return super.add(component, Integer.valueOf(0))
+ }
+
+ override fun doLayout() {
+ layer.setBounds(0, 0, width, height)
+ }
+
+ override fun addNotify() {
+ super.addNotify()
+ super.add(layer, Integer.valueOf(10))
+ }
+
+ override fun removeNotify() {
+ layer.dispose()
+ super.removeNotify()
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt
new file mode 100644
index 00000000..8bfc0763
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/SwingOpenCvViewport.kt
@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2023 OpenFTC Team & EOCV-Sim implementation by Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+package io.github.deltacv.vision.external.gui
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import io.github.deltacv.common.image.MatPoster
+import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue
+import org.jetbrains.skia.Color
+import org.jetbrains.skiko.GenericSkikoView
+import org.jetbrains.skiko.SkiaLayer
+import org.jetbrains.skiko.SkikoView
+import org.opencv.android.Utils
+import org.opencv.core.Mat
+import org.opencv.core.Size
+import org.openftc.easyopencv.MatRecycler
+import org.openftc.easyopencv.OpenCvCamera.ViewportRenderingPolicy
+import org.openftc.easyopencv.OpenCvViewRenderer
+import org.openftc.easyopencv.OpenCvViewport
+import org.openftc.easyopencv.OpenCvViewport.OptimizedRotation
+import org.openftc.easyopencv.OpenCvViewport.RenderHook
+import org.slf4j.LoggerFactory
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.TimeUnit
+import javax.swing.JComponent
+import javax.swing.SwingUtilities
+import kotlin.jvm.Throws
+
+class SwingOpenCvViewport(size: Size, fpsMeterDescriptor: String = "deltacv Vision") : OpenCvViewport, MatPoster {
+
+ private val syncObj = Any()
+
+ @Volatile
+ private var userRequestedActive = false
+
+ @Volatile
+ private var userRequestedPause = false
+ private val needToDeactivateRegardlessOfUser = false
+ private var surfaceExistsAndIsReady = false
+
+ @Volatile
+ private var useGpuCanvas = false
+
+ var dark = false
+
+ private enum class RenderingState {
+ STOPPED,
+ ACTIVE,
+ PAUSED
+ }
+
+ private val visionPreviewFrameQueue = EvictingBlockingQueue(ArrayBlockingQueue(VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 1))
+ private var framebufferRecycler: MatRecycler? = null
+
+ @Volatile
+ private var internalRenderingState = RenderingState.STOPPED
+ val renderer: OpenCvViewRenderer = OpenCvViewRenderer(false, fpsMeterDescriptor)
+
+ private val outputPosters = mutableListOf()
+
+ private val skiaLayer = SkiaLayer()
+ val component: JComponent get() = skiaLayer
+
+ var logger = LoggerFactory.getLogger(javaClass)
+
+ private var renderHook: RenderHook? = null
+
+ init {
+ visionPreviewFrameQueue.setEvictAction { value: MatRecycler.RecyclableMat? ->
+ /*
+ * If a Mat is evicted from the queue, we need
+ * to make sure to return it to the Mat recycler
+ */
+ framebufferRecycler!!.returnMat(value)
+ }
+
+ skiaLayer.skikoView = GenericSkikoView(skiaLayer, object: SkikoView {
+ override fun onRender(canvas: org.jetbrains.skia.Canvas, width: Int, height: Int, nanoTime: Long) {
+ renderCanvas(Canvas(canvas, width, height))
+
+ if(outputPosters.isNotEmpty()) {
+ synchronized(outputPosters) {
+ skiaLayer.screenshot().use { bmp ->
+ framebufferRecycler?.takeMatOrNull().let { mat ->
+ Utils.bitmapToMat(Bitmap(bmp), mat)
+
+ outputPosters.forEach { poster ->
+ poster.post(mat)
+ }
+
+ framebufferRecycler?.returnMat(mat)
+ }
+ }
+ }
+ }
+ }
+ })
+
+ setSize(size.width.toInt(), size.height.toInt())
+ }
+
+ var shouldPaintOrange = true
+
+ fun attachTo(component: Any) {
+ skiaLayer.attachTo(component)
+
+ SwingUtilities.invokeLater {
+ skiaLayer.needRedraw()
+ }
+ }
+
+ fun attachOutputPoster(poster: MatPoster) {
+ synchronized(outputPosters) {
+ outputPosters.add(poster)
+ }
+ }
+
+ fun detachOutputPoster(poster: MatPoster) {
+ synchronized(outputPosters) {
+ outputPosters.remove(poster)
+ }
+ }
+
+ fun skiaPanel() = SkiaPanel(skiaLayer)
+
+ override fun setSize(width: Int, height: Int) {
+ synchronized(syncObj) {
+ check(internalRenderingState == RenderingState.STOPPED) { "Cannot set size while renderer is active!" }
+
+ //Make sure we don't have any mats hanging around
+ //from when we might have been running before
+ visionPreviewFrameQueue.clear()
+ framebufferRecycler = MatRecycler(FRAMEBUFFER_RECYCLER_CAPACITY)
+
+ SwingUtilities.invokeLater {
+ skiaLayer.setSize(width, height)
+ skiaLayer.repaint()
+ }
+
+ surfaceExistsAndIsReady = true
+ checkState()
+ }
+ }
+
+ override fun setFpsMeterEnabled(enabled: Boolean) {}
+ override fun resume() {
+ synchronized(syncObj) {
+ userRequestedPause = false
+ checkState()
+ }
+ }
+
+ override fun pause() {
+ synchronized(syncObj) {
+ userRequestedPause = true
+ checkState()
+ }
+ }
+
+ /***
+ * Activate the render thread
+ */
+ @Synchronized
+ override fun activate() {
+ synchronized(syncObj) {
+ userRequestedActive = true
+ checkState()
+ }
+ }
+
+ /***
+ * Deactivate the render thread
+ */
+ override fun deactivate() {
+ synchronized(syncObj) {
+ userRequestedActive = false
+ checkState()
+ }
+ }
+
+ override fun setOptimizedViewRotation(rotation: OptimizedRotation) {}
+ override fun notifyStatistics(fps: Float, pipelineMs: Int, overheadMs: Int) {
+ renderer.notifyStatistics(fps, pipelineMs, overheadMs)
+ }
+
+ override fun setRecording(recording: Boolean) {}
+
+ override fun post(mat: Mat, userContext: Any) {
+ synchronized(syncObj) {
+ //did they give us null?
+ requireNotNull(mat) {
+ //ugh, they did
+ "cannot post null mat!"
+ }
+
+ //Are we actually rendering to the display right now? If not,
+ //no need to waste time doing a memcpy
+ if (internalRenderingState == RenderingState.ACTIVE) {
+ /*
+ * We need to copy this mat before adding it to the queue,
+ * because the pointer that was passed in here is only known
+ * to be pointing to a certain frame while we're executing.
+ */
+
+ /*
+ * Grab a framebuffer Mat from the recycler
+ * instead of doing a new alloc and then having
+ * to free it after rendering/eviction from queue
+ */
+ val matToCopyTo = framebufferRecycler!!.takeMatOrInterrupt()
+
+ mat.copyTo(matToCopyTo)
+ matToCopyTo.context = userContext
+
+ visionPreviewFrameQueue.offer(matToCopyTo)
+ }
+ }
+ }
+
+ /*
+ * Called with syncObj held
+ */
+ fun checkState() {
+ /*
+ * If the surface isn't ready, don't do anything
+ */
+ if (!surfaceExistsAndIsReady) {
+ logger.info("CheckState(): surface not ready or doesn't exist")
+ return
+ }
+
+ /*
+ * Does the user want us to stop?
+ */if (!userRequestedActive || needToDeactivateRegardlessOfUser) {
+ if (needToDeactivateRegardlessOfUser) {
+ logger.info("CheckState(): lifecycle mandates deactivation regardless of user")
+ } else {
+ logger.info("CheckState(): user requested that we deactivate")
+ }
+
+ /*
+ * We only need to stop the render thread if it's not
+ * already stopped
+ */if (internalRenderingState != RenderingState.STOPPED) {
+ logger.info("CheckState(): deactivating viewport")
+
+ /*
+ * Wait for him to die non-interuptibly
+ */
+ internalRenderingState = RenderingState.STOPPED
+ } else {
+ logger.info("CheckState(): already deactivated")
+ }
+ } else if (userRequestedActive) {
+ logger.info("CheckState(): user requested that we activate")
+
+ /*
+ * We only need to start the render thread if it's
+ * stopped.
+ */if (internalRenderingState == RenderingState.STOPPED) {
+ logger.info("CheckState(): activating viewport")
+ internalRenderingState = RenderingState.PAUSED
+ internalRenderingState = if (userRequestedPause) {
+ RenderingState.PAUSED
+ } else {
+ RenderingState.ACTIVE
+ }
+ } else {
+ logger.info("CheckState(): already activated")
+ }
+ }
+ if (internalRenderingState != RenderingState.STOPPED) {
+ if (userRequestedPause && internalRenderingState != RenderingState.PAUSED
+ || !userRequestedPause && internalRenderingState != RenderingState.ACTIVE) {
+ internalRenderingState = if (userRequestedPause) {
+ logger.info("CheckState(): pausing viewport")
+ RenderingState.PAUSED
+ } else {
+ logger.info("CheckState(): resuming viewport")
+ RenderingState.ACTIVE
+ }
+
+ /*
+ * Interrupt him so that he's not stuck looking at his frame queue.
+ * (We stop filling the frame queue if the user requested pause so
+ * we aren't doing pointless memcpys)
+ */
+ }
+ }
+ }
+
+ private val canvasLock = Any()
+ private lateinit var lastFrame: MatRecycler.RecyclableMat
+
+ private fun renderCanvas(canvas: Canvas) {
+ if(!::lastFrame.isInitialized) {
+ lastFrame = framebufferRecycler!!.takeMatOrNull()
+ }
+
+ synchronized(canvasLock) {
+ if(dark) {
+ canvas.drawColor(Color.BLACK)
+ } else {
+ canvas.drawColor(Color.WHITE)
+ }
+
+ when (internalRenderingState) {
+ RenderingState.ACTIVE -> {
+ shouldPaintOrange = true
+
+ val mat: MatRecycler.RecyclableMat = try {
+ //Grab a Mat from the frame queue
+ val frame = visionPreviewFrameQueue.poll(10, TimeUnit.MILLISECONDS) ?: lastFrame
+
+ frame
+ } catch (e: InterruptedException) {
+
+ //Note: we actually don't re-interrupt ourselves here, because interrupts are also
+ //used to simply make sure we properly pick up a transition to the PAUSED state, not
+ //just when we're trying to close. If we're trying to close, then exitRequested will
+ //be set, and since we break immediately right here, the close will be handled cleanly.
+ //Thread.currentThread().interrupt();
+ return
+ }
+
+ mat.copyTo(lastFrame)
+
+ if (mat.empty()) {
+ return // nope out
+ }
+
+ /*
+ * For some reason, the canvas will very occasionally be null upon closing.
+ * Stack Overflow seems to suggest this means the canvas has been destroyed.
+ * However, surfaceDestroyed(), which is called right before the surface is
+ * destroyed, calls checkState(), which *SHOULD* block until we die. This
+ * works most of the time, but not always? We don't yet understand...
+ */
+ if (canvas != null) {
+ renderer.render(mat, canvas, renderHook, mat.context)
+ } else {
+ logger.info("Canvas was null")
+ }
+
+ //We're done with that Mat object; return it to the Mat recycler so it can be used again later
+ if (mat !== lastFrame) {
+ framebufferRecycler!!.returnMat(mat)
+ }
+ }
+
+ RenderingState.PAUSED -> {
+ if (shouldPaintOrange) {
+ shouldPaintOrange = false
+
+ /*
+ * For some reason, the canvas will very occasionally be null upon closing.
+ * Stack Overflow seems to suggest this means the canvas has been destroyed.
+ * However, surfaceDestroyed(), which is called right before the surface is
+ * destroyed, calls checkState(), which *SHOULD* block until we die. This
+ * works most of the time, but not always? We don't yet understand...
+ */
+ if (canvas != null) {
+ renderer.renderPaused(canvas)
+ }
+ }
+ }
+
+ else -> {}
+ }
+ }
+ }
+
+ fun clearViewport() {
+ visionPreviewFrameQueue.clear()
+
+ synchronized(canvasLock) {
+ lastFrame.release()
+ }
+ }
+
+ override fun setRenderingPolicy(policy: ViewportRenderingPolicy) {}
+ override fun setRenderHook(renderHook: RenderHook) {
+ this.renderHook = renderHook
+ }
+
+ fun pollLastFrame(dst: Mat) {
+ synchronized(canvasLock) {
+ lastFrame.copyTo(dst)
+ }
+ }
+
+ companion object {
+ private const val VISION_PREVIEW_FRAME_QUEUE_CAPACITY = 2
+ private const val FRAMEBUFFER_RECYCLER_CAPACITY = VISION_PREVIEW_FRAME_QUEUE_CAPACITY + 4 //So that the evicting queue can be full, and the render thread has one checked out (+1) and post() can still take one (+1).
+ }
+}
\ No newline at end of file
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java b/Vision/src/main/java/io/github/deltacv/vision/external/gui/component/ImageX.java
similarity index 86%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java
rename to Vision/src/main/java/io/github/deltacv/vision/external/gui/component/ImageX.java
index 7a79b711..4fcb214d 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/component/ImageX.java
@@ -1,88 +1,87 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-
-package com.github.serivesmejia.eocvsim.gui.component;
-
-import com.github.serivesmejia.eocvsim.gui.util.GuiUtil;
-import com.github.serivesmejia.eocvsim.util.CvUtil;
-import org.opencv.core.Mat;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-
-public class ImageX extends JLabel {
-
- volatile ImageIcon icon;
-
- public ImageX() {
- super();
- }
-
- public ImageX(ImageIcon img) {
- this();
- setImage(img);
- }
-
- public ImageX(BufferedImage img) {
- this();
- setImage(img);
- }
-
- public void setImage(ImageIcon img) {
- if (icon != null)
- icon.getImage().flush(); //flush old image :p
-
- icon = img;
-
- setIcon(icon); //set to the new image
- }
-
- public synchronized void setImage(BufferedImage img) {
- Graphics2D g2d = (Graphics2D) getGraphics();
- g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-
- setImage(new ImageIcon(img)); //set to the new image
- }
-
- public synchronized void setImageMat(Mat m) {
- setImage(CvUtil.matToBufferedImage(m));
- }
-
- public synchronized BufferedImage getImage() {
- return (BufferedImage)icon.getImage();
- }
-
- @Override
- public void setSize(int width, int height) {
- super.setSize(width, height);
- setImage(GuiUtil.scaleImage(icon, width, height)); //set to the new image
- }
-
- @Override
- public void setSize(Dimension dimension) {
- super.setSize(dimension);
- setImage(GuiUtil.scaleImage(icon, (int)dimension.getWidth(), (int)dimension.getHeight())); //set to the new image
- }
-
-}
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.gui.component;
+
+import io.github.deltacv.vision.external.gui.util.ImgUtil;
+import io.github.deltacv.vision.external.util.CvUtil;
+import org.opencv.core.Mat;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+
+public class ImageX extends JLabel {
+ volatile ImageIcon icon;
+
+ public ImageX() {
+ super();
+ }
+
+ public ImageX(ImageIcon img) {
+ this();
+ setImage(img);
+ }
+
+ public ImageX(BufferedImage img) {
+ this();
+ setImage(img);
+ }
+
+ public void setImage(ImageIcon img) {
+ if (icon != null)
+ icon.getImage().flush(); //flush old image :p
+
+ icon = img;
+
+ setIcon(icon); //set to the new image
+ }
+
+ public synchronized void setImage(BufferedImage img) {
+ Graphics2D g2d = (Graphics2D) getGraphics();
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+ setImage(new ImageIcon(img)); //set to the new image
+ }
+
+ public synchronized void setImageMat(Mat m) {
+ setImage(CvUtil.matToBufferedImage(m));
+ }
+
+ public synchronized BufferedImage getImage() {
+ return (BufferedImage)icon.getImage();
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ super.setSize(width, height);
+ setImage(ImgUtil.scaleImage(icon, width, height)); //set to the new image
+ }
+
+ @Override
+ public void setSize(Dimension dimension) {
+ super.setSize(dimension);
+ setImage(ImgUtil.scaleImage(icon, (int)dimension.getWidth(), (int)dimension.getHeight())); //set to the new image
+ }
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/gui/util/ImgUtil.java b/Vision/src/main/java/io/github/deltacv/vision/external/gui/util/ImgUtil.java
new file mode 100644
index 00000000..d8e3da2b
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/gui/util/ImgUtil.java
@@ -0,0 +1,28 @@
+package io.github.deltacv.vision.external.gui.util;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class ImgUtil {
+
+ private ImgUtil() { }
+
+ public static ImageIcon scaleImage(ImageIcon icon, int w, int h) {
+
+ int nw = icon.getIconWidth();
+ int nh = icon.getIconHeight();
+
+ if (icon.getIconWidth() > w) {
+ nw = w;
+ nh = (nw * icon.getIconHeight()) / icon.getIconWidth();
+ }
+
+ if (nh > h) {
+ nh = h;
+ nw = (icon.getIconWidth() * nh) / icon.getIconHeight();
+ }
+
+ return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_SMOOTH));
+ }
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java
new file mode 100644
index 00000000..5ad77b76
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ThreadSourceHander.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.source;
+
+import java.util.HashMap;
+
+public class ThreadSourceHander {
+
+ private static HashMap handlers = new HashMap<>();
+
+ private ThreadSourceHander() {} // No instantiation
+
+ public static void register(Thread thread, VisionSourceHander handler) {
+ handlers.put(thread, handler);
+ }
+
+ public static void register(VisionSourceHander hander) {
+ register(Thread.currentThread(), hander);
+ }
+
+ public static VisionSourceHander threadHander() {
+ return handlers.get(Thread.currentThread());
+ }
+
+ public static VisionSource hand(String name) {
+ return threadHander().hand(name);
+ }
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java
new file mode 100644
index 00000000..b0bc6a81
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/ViewportAndSourceHander.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.source;
+
+import org.openftc.easyopencv.OpenCvViewport;
+
+public interface ViewportAndSourceHander extends VisionSourceHander {
+
+ OpenCvViewport viewport();
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java
new file mode 100644
index 00000000..213251b0
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSource.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.source;
+
+import org.opencv.core.Size;
+
+public interface VisionSource {
+
+ int init();
+
+ boolean start(Size requestedSize);
+
+ boolean attach(VisionSourced sourced);
+ boolean remove(VisionSourced sourced);
+
+ boolean stop();
+
+ boolean close();
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java
new file mode 100644
index 00000000..269430ce
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceBase.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.source;
+
+import io.github.deltacv.vision.external.util.Timestamped;
+import org.opencv.core.Mat;
+import org.opencv.core.Size;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public abstract class VisionSourceBase implements VisionSource {
+
+ private final Object lock = new Object();
+
+ ArrayList sourceds = new ArrayList<>();
+
+ SourceBaseHelperThread helperThread = new SourceBaseHelperThread(this);
+
+ @Override
+ public final boolean start(Size size) {
+ boolean result = startSource(size);
+
+ helperThread.start();
+
+ return result;
+ }
+
+ public abstract boolean startSource(Size size);
+
+ @Override
+ public boolean attach(VisionSourced sourced) {
+ synchronized (lock) {
+ return sourceds.add(sourced);
+ }
+ }
+
+ @Override
+ public boolean remove(VisionSourced sourced) {
+ synchronized (lock) {
+ return sourceds.remove(sourced);
+ }
+ }
+
+ @Override
+ public final boolean stop() {
+ if(!helperThread.isAlive() || helperThread.isInterrupted()) return false;
+
+ helperThread.interrupt();
+
+ return stopSource();
+ }
+
+ public abstract boolean stopSource();
+
+ public abstract Timestamped pullFrame();
+
+ private Timestamped pullFrameInternal() {
+ for(VisionSourced sourced : sourceds) {
+ synchronized (sourced) {
+ sourced.onFrameStart();
+ }
+ }
+
+ return pullFrame();
+ }
+
+ private static class SourceBaseHelperThread extends Thread {
+
+ VisionSourceBase sourceBase;
+ boolean shouldStop = false;
+
+ Logger logger;
+
+ public SourceBaseHelperThread(VisionSourceBase sourcedBase) {
+ super("Thread-SourceBaseHelper-" + sourcedBase.getClass().getSimpleName());
+ logger = LoggerFactory.getLogger(getName());
+
+ this.sourceBase = sourcedBase;
+ }
+
+ @Override
+ public void run() {
+ VisionSourced[] sourceds = new VisionSourced[0];
+
+ logger.info("starting");
+
+ while (!isInterrupted() && !shouldStop) {
+ Timestamped frame = sourceBase.pullFrameInternal();
+
+ synchronized (sourceBase.lock) {
+ sourceds = sourceBase.sourceds.toArray(new VisionSourced[0]);
+ }
+
+ for (VisionSourced sourced : sourceds) {
+ sourced.onNewFrame(frame.getValue(), frame.getTimestamp());
+ }
+ }
+
+ for(VisionSourced sourced : sourceds) {
+ sourced.stop();
+ }
+
+ logger.info("stop");
+ }
+ }
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java
new file mode 100644
index 00000000..b4fd133a
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourceHander.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.source;
+
+public interface VisionSourceHander {
+
+ VisionSource hand(String name);
+
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java
new file mode 100644
index 00000000..4274c289
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/source/VisionSourced.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.source;
+
+import org.opencv.core.Mat;
+
+public interface VisionSourced {
+
+ default void onFrameStart() {}
+
+ void stop();
+
+ void onNewFrame(Mat frame, long timestamp);
+
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/CvUtil.java
similarity index 98%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java
rename to Vision/src/main/java/io/github/deltacv/vision/external/util/CvUtil.java
index 491cc7cf..b132a8bd 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/CvUtil.java
@@ -21,9 +21,9 @@
*
*/
-package com.github.serivesmejia.eocvsim.util;
+package io.github.deltacv.vision.external.util;
-import com.github.serivesmejia.eocvsim.util.extension.CvExt;
+import io.github.deltacv.vision.external.util.extension.CvExt;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.Size;
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java
new file mode 100644
index 00000000..ec68e295
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/FrameQueue.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.util;
+
+import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue;
+import org.opencv.core.Mat;
+import org.openftc.easyopencv.MatRecycler;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+public class FrameQueue {
+
+ private final EvictingBlockingQueue viewportQueue;
+ private final MatRecycler matRecycler;
+
+ public FrameQueue(int maxQueueItems) {
+ viewportQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems));
+ matRecycler = new MatRecycler(maxQueueItems + 2);
+
+ viewportQueue.setEvictAction(this::evict);
+ }
+
+ public Mat takeMatAndPost() {
+ Mat mat = matRecycler.takeMatOrNull();
+ viewportQueue.add(mat);
+
+ return mat;
+ }
+
+ public Mat takeMat() {
+ return matRecycler.takeMatOrNull();
+ }
+
+ public Mat poll() {
+ Mat mat = viewportQueue.poll();
+
+ if(mat != null) {
+ evict(mat);
+ }
+
+ return mat;
+ }
+
+ private void evict(Mat mat) {
+ if(mat instanceof MatRecycler.RecyclableMat) {
+ matRecycler.returnMat((MatRecycler.RecyclableMat) mat);
+ }
+ }
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java b/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java
new file mode 100644
index 00000000..719fc0ee
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/Timestamped.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.external.util;
+
+public class Timestamped {
+
+ private final T value;
+ private final long timestamp;
+
+ public Timestamped(T value, long timestamp) {
+ this.value = value;
+ this.timestamp = timestamp;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+}
diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt b/Vision/src/main/java/io/github/deltacv/vision/external/util/extension/CvExt.kt
similarity index 94%
rename from EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt
rename to Vision/src/main/java/io/github/deltacv/vision/external/util/extension/CvExt.kt
index c192ca07..4e11ae9b 100644
--- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt
+++ b/Vision/src/main/java/io/github/deltacv/vision/external/util/extension/CvExt.kt
@@ -1,52 +1,52 @@
-/*
- * Copyright (c) 2021 Sebastian Erives
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- *
- */
-@file:JvmName("CvExt")
-
-package com.github.serivesmejia.eocvsim.util.extension
-
-import com.qualcomm.robotcore.util.Range
-import org.opencv.core.CvType
-import org.opencv.core.Mat
-import org.opencv.core.Scalar
-import org.opencv.core.Size
-import org.opencv.imgproc.Imgproc
-
-fun Scalar.cvtColor(code: Int): Scalar {
- val mat = Mat(5, 5, CvType.CV_8UC3);
- mat.setTo(this)
- Imgproc.cvtColor(mat, mat, code);
-
- val newScalar = Scalar(mat.get(1, 1))
- mat.release()
-
- return newScalar
-}
-
-fun Size.aspectRatio() = height / width
-fun Mat.aspectRatio() = size().aspectRatio()
-
-fun Size.clipTo(size: Size): Size {
- width = Range.clip(width, 0.0, size.width)
- height = Range.clip(height, 0.0, size.height)
- return this
+/*
+ * Copyright (c) 2021 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+@file:JvmName("CvExt")
+
+package io.github.deltacv.vision.external.util.extension
+
+import com.qualcomm.robotcore.util.Range
+import org.opencv.core.CvType
+import org.opencv.core.Mat
+import org.opencv.core.Scalar
+import org.opencv.core.Size
+import org.opencv.imgproc.Imgproc
+
+fun Scalar.cvtColor(code: Int): Scalar {
+ val mat = Mat(5, 5, CvType.CV_8UC3);
+ mat.setTo(this)
+ Imgproc.cvtColor(mat, mat, code);
+
+ val newScalar = Scalar(mat.get(1, 1))
+ mat.release()
+
+ return newScalar
+}
+
+fun Size.aspectRatio() = height / width
+fun Mat.aspectRatio() = size().aspectRatio()
+
+fun Size.clipTo(size: Size): Size {
+ width = Range.clip(width, 0.0, size.width)
+ height = Range.clip(height, 0.0, size.height)
+ return this
}
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt
new file mode 100644
index 00000000..aef5400b
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/Enums.kt
@@ -0,0 +1,5 @@
+package io.github.deltacv.vision.internal.opmode
+
+enum class OpModeNotification { INIT, START, STOP, NOTHING }
+
+enum class OpModeState { SELECTED, INIT, START, STOP, STOPPED }
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt
new file mode 100644
index 00000000..cade7d42
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/internal/opmode/OpModeNotifier.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.internal.opmode
+
+import com.github.serivesmejia.eocvsim.util.event.EventHandler
+import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue
+import java.util.concurrent.ArrayBlockingQueue
+
+class OpModeNotifier(maxNotificationsQueueSize: Int = 100) {
+
+ val notifications = EvictingBlockingQueue(ArrayBlockingQueue(maxNotificationsQueueSize))
+
+ private val stateLock = Any()
+ var state = OpModeState.STOPPED
+ private set
+ get() {
+ synchronized(stateLock) {
+ return field
+ }
+ }
+
+ val onStateChange = EventHandler("OpModeNotifier-onStateChange")
+
+ fun notify(notification: OpModeNotification) {
+ notifications.offer(notification)
+ }
+
+ fun notify(state: OpModeState) {
+ synchronized(stateLock) {
+ this.state = state
+ }
+
+ onStateChange.run()
+ }
+
+ fun notify(notification: OpModeNotification, state: OpModeState) {
+ notifications.offer(notification)
+
+ synchronized(stateLock) {
+ this.state = state
+ }
+ onStateChange.run()
+ }
+
+ fun reset() {
+ notifications.clear()
+ state = OpModeState.STOPPED
+ }
+
+ fun poll(): OpModeNotification {
+ return notifications.poll() ?: OpModeNotification.NOTHING
+ }
+
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java
new file mode 100644
index 00000000..3b1f965a
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraName.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.internal.source.ftc;
+
+import io.github.deltacv.vision.external.source.VisionSource;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraCharacteristics;
+
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+public abstract class SourcedCameraName implements WebcamName {
+
+ public abstract VisionSource getSource();
+
+ @Override
+ public boolean isWebcam() {
+ return false;
+ }
+
+ @Override
+ public boolean isCameraDirection() {
+ return false;
+ }
+
+ @Override
+ public boolean isSwitchable() {
+ return false;
+ }
+
+ @Override
+ public boolean isUnknown() {
+ return false;
+ }
+
+ @Override
+ public CameraCharacteristics getCameraCharacteristics() {
+ return null;
+ }
+
+}
diff --git a/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java
new file mode 100644
index 00000000..6efc4e02
--- /dev/null
+++ b/Vision/src/main/java/io/github/deltacv/vision/internal/source/ftc/SourcedCameraNameImpl.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package io.github.deltacv.vision.internal.source.ftc;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.qualcomm.robotcore.util.SerialNumber;
+import io.github.deltacv.vision.external.source.VisionSource;
+import org.jetbrains.annotations.NotNull;
+
+public class SourcedCameraNameImpl extends SourcedCameraName {
+
+ private VisionSource source;
+
+ public SourcedCameraNameImpl(VisionSource source) {
+ this.source = source;
+ }
+
+ @Override
+ public VisionSource getSource() {
+ return source;
+ }
+
+ @Override
+ public Manufacturer getManufacturer() {
+ return null;
+ }
+
+ @Override
+ public String getDeviceName() {
+ return null;
+ }
+
+ @Override
+ public String getConnectionInfo() {
+ return null;
+ }
+
+ @Override
+ public int getVersion() {
+ return 0;
+ }
+
+ @Override
+ public void resetDeviceConfigurationForOpMode() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @NonNull
+ @NotNull
+ @Override
+ public SerialNumber getSerialNumber() {
+ return null;
+ }
+
+ @Nullable
+ @org.jetbrains.annotations.Nullable
+ @Override
+ public String getUsbDeviceNameIfAttached() {
+ return null;
+ }
+
+ @Override
+ public boolean isAttached() {
+ return false;
+ }
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java
new file mode 100644
index 00000000..2e2eb352
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/BuiltinCameraDirection.java
@@ -0,0 +1,7 @@
+package org.firstinspires.ftc.robotcore.external.hardware.camera;
+
+public enum BuiltinCameraDirection
+{
+ BACK,
+ FRONT
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java
new file mode 100644
index 00000000..649f56c1
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraCharacteristics.java
@@ -0,0 +1,159 @@
+/*
+Copyright (c) 2017 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.external.hardware.camera;
+
+
+import com.qualcomm.robotcore.util.ElapsedTime;
+
+import org.firstinspires.ftc.robotcore.external.android.util.Size;
+import org.firstinspires.ftc.robotcore.internal.system.Misc;
+
+import java.util.List;
+
+/**
+ * THIS INTERFACE IS EXPERIMENTAL. Its form and function may change in whole or in part
+ * before being finalized for production use. Caveat emptor.
+ *
+ * Metadata regarding the configuration of video streams that a camera might produce.
+ * Modelled after {link android.hardware.camera2.params.StreamConfigurationMap}, though
+ * significantly simplified here.
+ */
+@SuppressWarnings("WeakerAccess")
+public interface CameraCharacteristics
+{
+ //----------------------------------------------------------------------------------------------
+ // Accessing
+ //----------------------------------------------------------------------------------------------
+
+ /**
+ * Get the image {@code format} output formats in this camera
+ */
+ int[] getAndroidFormats();
+
+ /**
+ * Get a list of sizes compatible with the requested image {@code format}.
+ *
+ * The {@code format} should be a supported format (one of the formats returned by
+ * {@link #getAndroidFormats}).
+ *
+ * @param androidFormat an image format from {link ImageFormat} or {link PixelFormat}
+ * @return
+ * an array of supported sizes,
+ * or {@code null} if the {@code format} is not a supported output
+ *
+ * see ImageFormat
+ * see PixelFormat
+ * @see #getAndroidFormats
+ */
+ Size[] getSizes(int androidFormat);
+
+ /** Gets the device-recommended optimum size for the indicated format */
+ Size getDefaultSize(int androidFormat);
+
+ /**
+ * Get the minimum frame duration for the format/size combination (in nanoseconds).
+ *
+ * {@code format} should be one of the ones returned by {@link #getAndroidFormats()}.
+ * {@code size} should be one of the ones returned by {@link #getSizes(int)}.
+ *
+ * @param androidFormat an image format from {link ImageFormat} or {link PixelFormat}
+ * @param size an output-compatible size
+ * @return a minimum frame duration {@code >} 0 in nanoseconds, or
+ * 0 if the minimum frame duration is not available.
+ *
+ * @throws IllegalArgumentException if {@code format} or {@code size} was not supported
+ * @throws NullPointerException if {@code size} was {@code null}
+ *
+ * see ImageFormat
+ * see PixelFormat
+ */
+ long getMinFrameDuration(int androidFormat, Size size);
+
+ /**
+ * Returns the maximum fps rate supported for the given format.
+ *
+ * @return the maximum fps rate supported for the given format.
+ */
+ int getMaxFramesPerSecond(int androidFormat, Size size);
+
+
+
+ class CameraMode
+ {
+ public final int androidFormat;
+ public final Size size;
+ public final long nsFrameDuration;
+ public final int fps;
+ public final boolean isDefaultSize; // not used in equalitor
+
+ public CameraMode(int androidFormat, Size size, long nsFrameDuration, boolean isDefaultSize)
+ {
+ this.androidFormat = androidFormat;
+ this.size = size;
+ this.nsFrameDuration = nsFrameDuration;
+ this.fps = (int) (ElapsedTime.SECOND_IN_NANO / nsFrameDuration);
+ this.isDefaultSize = isDefaultSize;
+ }
+
+ @Override public String toString()
+ {
+ return Misc.formatInvariant("CameraMode(format=%d %dx%d fps=%d)", androidFormat, size.getWidth(), size.getHeight(), fps);
+ }
+
+ @Override public boolean equals(Object o)
+ {
+ if (o instanceof CameraMode)
+ {
+ CameraMode them = (CameraMode)o;
+ return androidFormat == them.androidFormat
+ && size.equals(them.size)
+ && nsFrameDuration == them.nsFrameDuration
+ && fps == them.fps;
+ }
+ else
+ return super.equals(o);
+ }
+
+ @Override public int hashCode()
+ {
+ return Integer.valueOf(androidFormat).hashCode() ^ size.hashCode() ^ Integer.valueOf(fps).hashCode();
+ }
+ }
+
+ /**
+ * Returns all the combinatorial format, size, and fps camera modes supported.
+ *
+ * @return all the combinatorial format, size, and fps camera modes supported
+ */
+ List getAllCameraModes();
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java
new file mode 100644
index 00000000..35ec8635
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraControls.java
@@ -0,0 +1,46 @@
+/*
+Copyright (c) 2017 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.external.hardware.camera;
+
+import androidx.annotation.Nullable;
+
+import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl;
+
+public interface CameraControls
+{
+ //----------------------------------------------------------------------------------------------
+ // Controls
+ //----------------------------------------------------------------------------------------------
+
+ @Nullable T getControl(Class controlType);
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java
new file mode 100644
index 00000000..42ffc7ed
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/CameraName.java
@@ -0,0 +1,84 @@
+/*
+Copyright (c) 2017 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.external.hardware.camera;
+
+import com.qualcomm.robotcore.hardware.HardwareDevice;
+
+import org.firstinspires.ftc.robotcore.external.function.Consumer;
+
+/**
+ * {@link CameraName} identifies a {@link HardwareDevice} which is a camera.
+ */
+public interface CameraName
+{
+ /**
+ * Returns whether or not this name is that of a webcam. If true, then the
+ * {@link CameraName} can be cast to a {@link WebcamName}.
+ *
+ * @return whether or not this name is that of a webcam
+ * @see WebcamName
+ */
+ boolean isWebcam();
+
+ /**
+ * Returns whether or not this name is that of a builtin phone camera. If true, then the
+ * {@link CameraName} can be cast to a {@link BuiltinCameraName}.
+ *
+ * @return whether or not this name is that of a builtin phone camera
+ */
+ boolean isCameraDirection();
+
+ /**
+ * Returns whether this name is one representing the ability to switch amongst a
+ * series of member cameras. If true, then the receiver can be cast to a
+ * {@link SwitchableCameraName}.
+ *
+ * @return whether this is a {@link SwitchableCameraName}
+ */
+ boolean isSwitchable();
+
+
+ /**
+ * Returns whether or not this name represents that of an unknown or indeterminate camera.
+ * @return whether or not this name represents that of an unknown or indeterminate camera
+ */
+ boolean isUnknown();
+
+ /**
+ * Query the capabilities of a camera device. These capabilities are
+ * immutable for a given camera.
+ *
+ * @return The properties of the given camera. A degenerate empty set of properties is returned on error.
+ */
+ CameraCharacteristics getCameraCharacteristics();
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java
new file mode 100644
index 00000000..f8de2587
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/WebcamName.java
@@ -0,0 +1,63 @@
+/*
+Copyright (c) 2018 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.external.hardware.camera;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.qualcomm.robotcore.hardware.HardwareDevice;
+import com.qualcomm.robotcore.util.SerialNumber;
+
+public interface WebcamName extends CameraName, HardwareDevice
+{
+ /**
+ * Returns the USB serial number of the webcam
+ * @return the USB serial number of the webcam
+ */
+ @NonNull SerialNumber getSerialNumber();
+
+ /**
+ * Returns the USB device path currently associated with this webcam.
+ * May be null if the webcam is not presently attached.
+ *
+ * @return returns the USB device path associated with this name.
+ * see UsbManager#getDeviceList()
+ */
+ @Nullable String getUsbDeviceNameIfAttached();
+
+ /**
+ * Returns whether this camera currently attached to the robot controller
+ * @return whether this camera currently attached to the robot controller
+ */
+ boolean isAttached();
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java
new file mode 100644
index 00000000..499d683f
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/robotcore/external/hardware/camera/controls/CameraControl.java
@@ -0,0 +1,42 @@
+/*
+Copyright (c) 2018 Robert Atkinson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted (subject to the limitations in the disclaimer below) provided that
+the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+Neither the name of Robert Atkinson nor the names of his contributors may be used to
+endorse or promote products derived from this software without specific prior
+written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package org.firstinspires.ftc.robotcore.external.hardware.camera.controls;
+
+/**
+ * A {@link CameraControl} can be thought of as a knob or setting or dial on a {link Camera}
+ * that can be adjusted by the user
+ */
+@SuppressWarnings("WeakerAccess")
+public interface CameraControl
+{
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java
new file mode 100644
index 00000000..e3dd8c0b
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortal.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision;
+
+import android.util.Size;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.github.deltacv.vision.external.source.ThreadSourceHander;
+import io.github.deltacv.vision.internal.source.ftc.SourcedCameraNameImpl;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.BuiltinCameraDirection;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl;
+import org.openftc.easyopencv.OpenCvCameraFactory;
+import org.openftc.easyopencv.OpenCvWebcam;
+
+public abstract class VisionPortal
+{
+
+ private static final int DEFAULT_VIEW_CONTAINER_ID = 0;
+
+ /**
+ * StreamFormat is only applicable if using a webcam
+ */
+ public enum StreamFormat
+ {
+ /** The only format that was supported historically; it is uncompressed but
+ * chroma subsampled and uses lots of bandwidth - this limits frame rate
+ * at higher resolutions and also limits the ability to use two cameras
+ * on the same bus to lower resolutions
+ */
+ YUY2(OpenCvWebcam.StreamFormat.YUY2),
+
+ /** Compressed motion JPEG stream format; allows for higher resolutions at
+ * full frame rate, and better ability to use two cameras on the same bus.
+ * Requires extra CPU time to run decompression routine.
+ */
+ MJPEG(OpenCvWebcam.StreamFormat.MJPEG);
+
+ final OpenCvWebcam.StreamFormat eocvStreamFormat;
+
+ StreamFormat(OpenCvWebcam.StreamFormat eocvStreamFormat)
+ {
+ this.eocvStreamFormat = eocvStreamFormat;
+ }
+ }
+
+ /**
+ * If you are using multiple vision portals with live previews concurrently,
+ * you need to split up the screen to make room for both portals
+ */
+ public enum MultiPortalLayout
+ {
+ /**
+ * Divides the screen vertically
+ */
+ VERTICAL(OpenCvCameraFactory.ViewportSplitMethod.VERTICALLY),
+
+ /**
+ * Divides the screen horizontally
+ */
+ HORIZONTAL(OpenCvCameraFactory.ViewportSplitMethod.HORIZONTALLY);
+
+ private final OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod;
+
+ MultiPortalLayout(OpenCvCameraFactory.ViewportSplitMethod viewportSplitMethod)
+ {
+ this.viewportSplitMethod = viewportSplitMethod;
+ }
+ }
+
+ /**
+ * Split up the screen for using multiple vision portals with live views simultaneously
+ * @param numPortals the number of portals to create space for on the screen
+ * @param mpl the methodology for laying out the multiple live views on the screen
+ * @return an array of view IDs, whose elements may be passed to {@link Builder#setCameraMonitorViewId(int)}
+ */
+ public static int[] makeMultiPortalView(int numPortals, MultiPortalLayout mpl)
+ {
+ throw new UnsupportedOperationException("Split views not supported yet");
+ }
+
+ /**
+ * Create a VisionPortal for an internal camera using default configuration parameters, and
+ * skipping the use of the {@link Builder} pattern.
+ * @param cameraDirection the internal camera to use
+ * @param processors all the processors you want to inject into the portal
+ * @return a configured, ready to use VisionPortal
+ */
+ public static VisionPortal easyCreateWithDefaults(BuiltinCameraDirection cameraDirection, VisionProcessor... processors)
+ {
+ return new Builder()
+ .setCamera(cameraDirection)
+ .addProcessors(processors)
+ .build();
+ }
+
+ /**
+ * Create a VisionPortal for a webcam using default configuration parameters, and
+ * skipping the use of the {@link Builder} pattern.
+ * @param processors all the processors you want to inject into the portal
+ * @return a configured, ready to use VisionPortal
+ */
+ public static VisionPortal easyCreateWithDefaults(CameraName cameraName, VisionProcessor... processors)
+ {
+ return new Builder()
+ .setCamera(cameraName)
+ .addProcessors(processors)
+ .build();
+ }
+
+ public static class Builder
+ {
+ // STATIC !
+ private static final ArrayList attachedProcessors = new ArrayList<>();
+
+ private CameraName camera;
+ private int cameraMonitorViewId = DEFAULT_VIEW_CONTAINER_ID; // 0 == none
+ private boolean autoStopLiveView = true;
+ private Size cameraResolution = new Size(640, 480);
+ private StreamFormat streamFormat = null;
+ private StreamFormat STREAM_FORMAT_DEFAULT = StreamFormat.YUY2;
+ private final List processors = new ArrayList<>();
+
+ /**
+ * Configure the portal to use a webcam
+ * @param camera the WebcamName of the camera to use
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setCamera(CameraName camera)
+ {
+ this.camera = camera;
+ return this;
+ }
+
+ /**
+ * Configure the portal to use an internal camera
+ * @param cameraDirection the internal camera to use
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setCamera(BuiltinCameraDirection cameraDirection)
+ {
+ this.camera = new SourcedCameraNameImpl(ThreadSourceHander.hand("default"));
+ return this;
+ }
+
+ /**
+ * Configure the vision portal to stream from the camera in a certain image format
+ * THIS APPLIES TO WEBCAMS ONLY!
+ * @param streamFormat the desired streaming format
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setStreamFormat(StreamFormat streamFormat)
+ {
+ this.streamFormat = streamFormat;
+ return this;
+ }
+
+ /**
+ * Configure the vision portal to use (or not to use) a live camera preview
+ * @param enableLiveView whether or not to use a live preview
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder enableCameraMonitoring(boolean enableLiveView)
+ {
+ setCameraMonitorViewId(1);
+ return this;
+ }
+
+ /**
+ * Configure whether the portal should automatically pause the live camera
+ * view if all attached processors are disabled; this can save computational resources
+ * @param autoPause whether to enable this feature or not
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setAutoStopLiveView(boolean autoPause)
+ {
+ this.autoStopLiveView = autoPause;
+ return this;
+ }
+
+ /**
+ * A more advanced version of {@link #enableCameraMonitoring(boolean)}; allows you
+ * to specify a specific view ID to use as a container, rather than just using the default one
+ * @param cameraMonitorViewId view ID of container for live view
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setCameraMonitorViewId(int cameraMonitorViewId)
+ {
+ this.cameraMonitorViewId = cameraMonitorViewId;
+ return this;
+ }
+
+ /**
+ * Specify the resolution in which to stream images from the camera. To find out what resolutions
+ * your camera supports, simply call this with some random numbers (e.g. new Size(4634, 11115))
+ * and the error message will provide a list of supported resolutions.
+ * @param cameraResolution the resolution in which to stream images from the camera
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setCameraResolution(Size cameraResolution)
+ {
+ this.cameraResolution = cameraResolution;
+ return this;
+ }
+
+ /**
+ * Send a {@link VisionProcessor} into this portal to allow it to process camera frames.
+ * @param processor the processor to attach
+ * @return the {@link Builder} object, to allow for method chaining
+ * @throws RuntimeException if the specified processor is already inside another portal
+ */
+ public Builder addProcessor(VisionProcessor processor)
+ {
+ synchronized (attachedProcessors)
+ {
+ if (attachedProcessors.contains(processor))
+ {
+ throw new RuntimeException("This VisionProcessor has already been attached to a VisionPortal, either a different one or perhaps even this same portal.");
+ }
+ else
+ {
+ attachedProcessors.add(processor);
+ }
+ }
+
+ processors.add(processor);
+ return this;
+ }
+
+ /**
+ * Send multiple {@link VisionProcessor}s into this portal to allow them to process camera frames.
+ * @param processors the processors to attach
+ * @return the {@link Builder} object, to allow for method chaining
+ * @throws RuntimeException if the specified processor is already inside another portal
+ */
+ public Builder addProcessors(VisionProcessor... processors)
+ {
+ for (VisionProcessor p : processors)
+ {
+ addProcessor(p);
+ }
+
+ return this;
+ }
+
+ /**
+ * Actually create the {@link VisionPortal} i.e. spool up the camera and live view
+ * and begin sending image data to any attached {@link VisionProcessor}s
+ * @return a configured, ready to use portal
+ * @throws RuntimeException if you didn't specify what camera to use
+ * @throws IllegalStateException if you tried to set the stream format when not using a webcam
+ */
+ public VisionPortal build()
+ {
+ if (camera == null)
+ {
+ throw new RuntimeException("You can't build a vision portal without setting a camera!");
+ }
+
+ if (streamFormat != null)
+ {
+ if (!camera.isWebcam() && !camera.isSwitchable())
+ {
+ throw new IllegalStateException("setStreamFormat() may only be used with a webcam");
+ }
+ }
+ else
+ {
+ // Only used with webcams, will be ignored for internal camera
+ streamFormat = STREAM_FORMAT_DEFAULT;
+ }
+
+ return new VisionPortalImpl(
+ camera, cameraMonitorViewId, autoStopLiveView, cameraResolution, streamFormat,
+ processors.toArray(new VisionProcessor[0]));
+ }
+ }
+
+ /**
+ * Enable or disable a {@link VisionProcessor} that is attached to this portal.
+ * Disabled processors are not passed new image data and do not consume any computational
+ * resources. Of course, they also don't give you any useful data when disabled.
+ * This takes effect immediately (on the next frame interval)
+ * @param processor the processor to enable or disable
+ * @param enabled should it be enabled or disabled?
+ * @throws IllegalArgumentException if the processor specified isn't inside this portal
+ */
+ public abstract void setProcessorEnabled(VisionProcessor processor, boolean enabled);
+
+ /**
+ * Queries whether a given processor is enabled
+ * @param processor the processor in question
+ * @return whether the processor in question is enabled
+ * @throws IllegalArgumentException if the processor specified isn't inside this portal
+ */
+ public abstract boolean getProcessorEnabled(VisionProcessor processor);
+
+ /**
+ * The various states that the camera may be in at any given time
+ */
+ public enum CameraState
+ {
+ /**
+ * The camera device handle is being opened
+ */
+ OPENING_CAMERA_DEVICE,
+
+ /**
+ * The camera device handle has been opened and the camera
+ * is now ready to start streaming
+ */
+ CAMERA_DEVICE_READY,
+
+ /**
+ * The camera stream is starting
+ */
+ STARTING_STREAM,
+
+ /**
+ * The camera streaming session is in flight and providing image data
+ * to any attached {@link VisionProcessor}s
+ */
+ STREAMING,
+
+ /**
+ * The camera stream is being shut down
+ */
+ STOPPING_STREAM,
+
+ /**
+ * The camera device handle is being closed
+ */
+ CLOSING_CAMERA_DEVICE,
+
+ /**
+ * The camera device handle has been closed; you must create a new
+ * portal if you wish to use the camera again
+ */
+ CAMERA_DEVICE_CLOSED,
+
+ /**
+ * The camera was having a bad day and refused to cooperate with configuration for either
+ * opening the device handle or starting the streaming session
+ */
+ ERROR
+ }
+
+ /**
+ * Query the current state of the camera (e.g. is a streaming session in flight?)
+ * @return the current state of the camera
+ */
+ public abstract CameraState getCameraState();
+
+ public abstract void saveNextFrameRaw(String filename);
+
+ /**
+ * Stop the streaming session. This is an asynchronous call which does not take effect
+ * immediately. You may use {@link #getCameraState()} to monitor for when this command
+ * has taken effect. If you call {@link #resumeStreaming()} before the operation is complete,
+ * it will SYNCHRONOUSLY await completion of the stop command
+ *
+ * Stopping the streaming session is a good way to save computational resources if there may
+ * be long (e.g. 10+ second) periods of match play in which vision processing is not required.
+ * When streaming is stopped, no new image data is acquired from the camera and any attached
+ * {@link VisionProcessor}s will lie dormant until such time as {@link #resumeStreaming()} is called.
+ *
+ * Stopping and starting the stream can take a second or two, and thus is not advised for use
+ * cases where instantaneously enabling/disabling vision processing is required.
+ */
+ public abstract void stopStreaming();
+
+ /**
+ * Resume the streaming session if previously stopped by {@link #stopStreaming()}. This is
+ * an asynchronous call which does not take effect immediately. If you call {@link #stopStreaming()}
+ * before the operation is complete, it will SYNCHRONOUSLY await completion of the resume command.
+ *
+ * See notes about use case on {@link #stopStreaming()}
+ */
+ public abstract void resumeStreaming();
+
+ /**
+ * Temporarily stop the live view on the RC screen. This DOES NOT affect the ability to get
+ * a camera frame on the Driver Station's "Camera Stream" feature.
+ *
+ * This has no effect if you didn't set up a live view.
+ *
+ * Stopping the live view is recommended during competition to save CPU resources when
+ * a live view is not required for debugging purposes.
+ */
+ public abstract void stopLiveView();
+
+ /**
+ * Start the live view again, if it was previously stopped with {@link #stopLiveView()}
+ *
+ * This has no effect if you didn't set up a live view.
+ */
+ public abstract void resumeLiveView();
+
+ /**
+ * Get the current rate at which frames are passing through the vision portal
+ * (and all processors therein) per second - frames per second
+ * @return the current vision frame rate in frames per second
+ */
+ public abstract float getFps();
+
+ /**
+ * Get a camera control handle
+ * ONLY APPLICABLE TO WEBCAMS
+ * @param controlType the type of control to get
+ * @return the requested control
+ * @throws UnsupportedOperationException if you are not using a webcam
+ */
+ public abstract T getCameraControl(Class controlType);
+
+ /**
+ * Switches the active camera to the indicated camera.
+ * ONLY APPLICABLE IF USING A SWITCHABLE WEBCAM
+ * @param webcamName the name of the to-be-activated camera
+ * @throws UnsupportedOperationException if you are not using a switchable webcam
+ */
+ public abstract void setActiveCamera(WebcamName webcamName);
+
+ /**
+ * Returns the name of the currently active camera
+ * ONLY APPLIES IF USING A SWITCHABLE WEBCAM
+ * @return the name of the currently active camera
+ * @throws UnsupportedOperationException if you are not using a switchable webcam
+ */
+ public abstract WebcamName getActiveCamera();
+
+ /**
+ * Teardown everything prior to the end of the OpMode (perhaps to save resources) at which point
+ * it will be torn down automagically anyway.
+ *
+ * This will stop all vision related processing, shut down the camera, and remove the live view.
+ * A closed portal may not be re-opened: if you wish to use the camera again, you must make a new portal
+ */
+ public abstract void close();
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java
new file mode 100644
index 00000000..a0455ccd
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionPortalImpl.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision;
+
+import android.graphics.Canvas;
+import android.util.Size;
+
+import com.qualcomm.robotcore.util.RobotLog;
+
+import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.CameraName;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.controls.CameraControl;
+import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration;
+import org.opencv.core.Mat;
+import org.openftc.easyopencv.OpenCvCamera;
+import org.openftc.easyopencv.OpenCvCameraFactory;
+import org.openftc.easyopencv.OpenCvCameraRotation;
+import org.openftc.easyopencv.OpenCvWebcam;
+import org.openftc.easyopencv.TimestampedOpenCvPipeline;
+
+public class VisionPortalImpl extends VisionPortal
+{
+ protected OpenCvCamera camera;
+ protected volatile CameraState cameraState = CameraState.CAMERA_DEVICE_CLOSED;
+ protected VisionProcessor[] processors;
+ protected volatile boolean[] processorsEnabled;
+ protected volatile CameraCalibration calibration;
+ protected final boolean autoPauseCameraMonitor;
+ protected final Object userStateMtx = new Object();
+ protected final Size cameraResolution;
+ protected final StreamFormat webcamStreamFormat;
+ protected static final OpenCvCameraRotation CAMERA_ROTATION = OpenCvCameraRotation.SENSOR_NATIVE;
+ protected String captureNextFrame;
+ protected final Object captureFrameMtx = new Object();
+
+ public VisionPortalImpl(CameraName camera, int cameraMonitorViewId, boolean autoPauseCameraMonitor, Size cameraResolution, StreamFormat webcamStreamFormat, VisionProcessor[] processors)
+ {
+ this.processors = processors;
+ this.cameraResolution = cameraResolution;
+ this.webcamStreamFormat = webcamStreamFormat;
+ processorsEnabled = new boolean[processors.length];
+
+ for (int i = 0; i < processors.length; i++)
+ {
+ processorsEnabled[i] = true;
+ }
+
+ this.autoPauseCameraMonitor = autoPauseCameraMonitor;
+
+ createCamera(camera, cameraMonitorViewId);
+ startCamera();
+ }
+
+ protected void startCamera()
+ {
+ if (camera == null)
+ {
+ throw new IllegalStateException("This should never happen");
+ }
+
+ if (cameraResolution == null) // was the user a silly silly
+ {
+ throw new IllegalArgumentException("parameters.cameraResolution == null");
+ }
+
+ camera.setViewportRenderer(OpenCvCamera.ViewportRenderer.NATIVE_VIEW);
+
+ if(!(camera instanceof OpenCvWebcam))
+ {
+ camera.setViewportRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW);
+ }
+
+ cameraState = CameraState.OPENING_CAMERA_DEVICE;
+ camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener()
+ {
+ @Override
+ public void onOpened()
+ {
+ cameraState = CameraState.CAMERA_DEVICE_READY;
+ cameraState = CameraState.STARTING_STREAM;
+
+ camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION);
+
+ camera.setPipeline(new ProcessingPipeline());
+ cameraState = CameraState.STREAMING;
+ }
+
+ @Override
+ public void onError(int errorCode)
+ {
+ cameraState = CameraState.ERROR;
+ RobotLog.ee("VisionPortalImpl", "Camera opening failed.");
+ }
+ });
+ }
+
+ protected void createCamera(CameraName cameraName, int cameraMonitorViewId)
+ {
+ if (cameraName == null) // was the user a silly silly
+ {
+ throw new IllegalArgumentException("parameters.camera == null");
+ }
+ else if (cameraName instanceof SourcedCameraName) // Webcams
+ {
+ camera = OpenCvCameraFactory.getInstance().createWebcam((WebcamName) cameraName, cameraMonitorViewId);
+ }
+ else // ¯\_(ツ)_/¯
+ {
+ throw new IllegalArgumentException("Unknown camera name");
+ }
+ }
+
+ @Override
+ public void setProcessorEnabled(VisionProcessor processor, boolean enabled)
+ {
+ int numProcessorsEnabled = 0;
+ boolean ok = false;
+
+ for (int i = 0; i < processors.length; i++)
+ {
+ if (processor == processors[i])
+ {
+ processorsEnabled[i] = enabled;
+ ok = true;
+ }
+
+ if (processorsEnabled[i])
+ {
+ numProcessorsEnabled++;
+ }
+ }
+
+ if (ok)
+ {
+ if (autoPauseCameraMonitor)
+ {
+ if (numProcessorsEnabled == 0)
+ {
+ camera.pauseViewport();
+ }
+ else
+ {
+ camera.resumeViewport();
+ }
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException("Processor not attached to this helper!");
+ }
+ }
+
+ @Override
+ public boolean getProcessorEnabled(VisionProcessor processor)
+ {
+ for (int i = 0; i < processors.length; i++)
+ {
+ if (processor == processors[i])
+ {
+ return processorsEnabled[i];
+ }
+ }
+
+ throw new IllegalArgumentException("Processor not attached to this helper!");
+ }
+
+ @Override
+ public CameraState getCameraState()
+ {
+ return cameraState;
+ }
+
+ @Override
+ public void setActiveCamera(WebcamName webcamName)
+ {
+ throw new UnsupportedOperationException("setActiveCamera is only supported for switchable webcams");
+ }
+
+ @Override
+ public WebcamName getActiveCamera()
+ {
+ throw new UnsupportedOperationException("getActiveCamera is only supported for switchable webcams");
+ }
+
+ @Override
+ public T getCameraControl(Class controlType)
+ {
+ if (cameraState == CameraState.STREAMING)
+ {
+ throw new UnsupportedOperationException("Getting controls is not yet supported in EOCV-Sim");
+ }
+ else
+ {
+ throw new IllegalStateException("You cannot use camera controls until the camera is streaming");
+ }
+ }
+
+ class ProcessingPipeline extends TimestampedOpenCvPipeline
+ {
+ @Override
+ public void init(Mat firstFrame)
+ {
+ for (VisionProcessor processor : processors)
+ {
+ processor.init(firstFrame.width(), firstFrame.height(), calibration);
+ }
+ }
+
+ @Override
+ public Mat processFrame(Mat input, long captureTimeNanos)
+ {
+ synchronized (captureFrameMtx)
+ {
+ if (captureNextFrame != null)
+ {
+ // saveMatToDiskFullPath(input, "/sdcard/VisionPortal-" + captureNextFrame + ".png");
+ }
+
+ captureNextFrame = null;
+ }
+
+ Object[] processorDrawCtxes = new Object[processors.length]; // cannot re-use frome to frame
+
+ for (int i = 0; i < processors.length; i++)
+ {
+ if (processorsEnabled[i])
+ {
+ processorDrawCtxes[i] = processors[i].processFrame(input, captureTimeNanos);
+ }
+ }
+
+ requestViewportDrawHook(processorDrawCtxes);
+
+ return input;
+ }
+
+ @Override
+ public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext)
+ {
+ Object[] ctx = (Object[]) userContext;
+
+ for (int i = 0; i < processors.length; i++)
+ {
+ if (processorsEnabled[i])
+ {
+ processors[i].onDrawFrame(canvas, onscreenWidth, onscreenHeight, scaleBmpPxToCanvasPx, scaleCanvasDensity, ctx[i]);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void saveNextFrameRaw(String filepath)
+ {
+ synchronized (captureFrameMtx)
+ {
+ captureNextFrame = filepath;
+ }
+ }
+
+ @Override
+ public void stopStreaming()
+ {
+ synchronized (userStateMtx)
+ {
+ if (cameraState == CameraState.STREAMING || cameraState == CameraState.STARTING_STREAM)
+ {
+ cameraState = CameraState.STOPPING_STREAM;
+ new Thread(() ->
+ {
+ synchronized (userStateMtx)
+ {
+ camera.stopStreaming();
+ cameraState = CameraState.CAMERA_DEVICE_READY;
+ }
+ }).start();
+ }
+ else if (cameraState == CameraState.STOPPING_STREAM
+ || cameraState == CameraState.CAMERA_DEVICE_READY
+ || cameraState == CameraState.CLOSING_CAMERA_DEVICE)
+ {
+ // be idempotent
+ }
+ else
+ {
+ throw new RuntimeException("Illegal CameraState when calling stopStreaming()");
+ }
+ }
+ }
+
+ @Override
+ public void resumeStreaming()
+ {
+ synchronized (userStateMtx)
+ {
+ if (cameraState == CameraState.CAMERA_DEVICE_READY || cameraState == CameraState.STOPPING_STREAM)
+ {
+ cameraState = CameraState.STARTING_STREAM;
+ new Thread(() ->
+ {
+ synchronized (userStateMtx)
+ {
+ if (camera instanceof OpenCvWebcam)
+ {
+ ((OpenCvWebcam)camera).startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION, webcamStreamFormat.eocvStreamFormat);
+ }
+ else
+ {
+ camera.startStreaming(cameraResolution.getWidth(), cameraResolution.getHeight(), CAMERA_ROTATION);
+ }
+ cameraState = CameraState.STREAMING;
+ }
+ }).start();
+ }
+ else if (cameraState == CameraState.STREAMING
+ || cameraState == CameraState.STARTING_STREAM
+ || cameraState == CameraState.OPENING_CAMERA_DEVICE) // we start streaming automatically after we open
+ {
+ // be idempotent
+ }
+ else
+ {
+ throw new RuntimeException("Illegal CameraState when calling stopStreaming()");
+ }
+ }
+ }
+
+ @Override
+ public void stopLiveView()
+ {
+ OpenCvCamera cameraSafe = camera;
+
+ if (cameraSafe != null)
+ {
+ camera.pauseViewport();
+ }
+ }
+
+ @Override
+ public void resumeLiveView()
+ {
+ OpenCvCamera cameraSafe = camera;
+
+ if (cameraSafe != null)
+ {
+ camera.resumeViewport();
+ }
+ }
+
+ @Override
+ public float getFps()
+ {
+ OpenCvCamera cameraSafe = camera;
+
+ if (cameraSafe != null)
+ {
+ return cameraSafe.getFps();
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ synchronized (userStateMtx)
+ {
+ cameraState = CameraState.CLOSING_CAMERA_DEVICE;
+
+ if (camera != null)
+ {
+ camera.closeCameraDeviceAsync(() -> cameraState = CameraState.CAMERA_DEVICE_CLOSED);
+ }
+
+ camera = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java
new file mode 100644
index 00000000..73784e91
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessor.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision;
+
+/**
+ * May be attached to a {@link VisionPortal} to run image processing
+ */
+public interface VisionProcessor extends VisionProcessorInternal {}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java
new file mode 100644
index 00000000..74202245
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/VisionProcessorInternal.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision;
+
+import android.graphics.Canvas;
+
+import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration;
+import org.opencv.core.Mat;
+
+/**
+ * Internal interface
+ */
+interface VisionProcessorInternal
+{
+ void init(int width, int height, CameraCalibration calibration);
+ Object processFrame(Mat frame, long captureTimeNanos);
+ void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext);
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java
new file mode 100644
index 00000000..484aca1e
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagCanvasAnnotator.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+
+import android.graphics.Typeface;
+import org.opencv.calib3d.Calib3d;
+import org.opencv.core.Mat;
+import org.opencv.core.MatOfDouble;
+import org.opencv.core.MatOfPoint2f;
+import org.opencv.core.MatOfPoint3f;
+import org.opencv.core.Point;
+import org.opencv.core.Point3;
+
+public class AprilTagCanvasAnnotator
+{
+ final Mat cameraMatrix;
+ float bmpPxToCanvasPx;
+ float canvasDensityScale;
+
+ LinePaint redAxisPaint = new LinePaint(Color.RED);
+ LinePaint greenAxisPaint = new LinePaint(Color.GREEN);
+ LinePaint blueAxisPaint = new LinePaint(Color.BLUE);
+
+ LinePaint boxPillarPaint = new LinePaint(Color.rgb(7,197,235));
+ LinePaint boxTopPaint = new LinePaint(Color.GREEN);
+
+ static class LinePaint extends Paint
+ {
+ public LinePaint(int color)
+ {
+ setColor(color);
+ setAntiAlias(true);
+ setStrokeCap(Paint.Cap.ROUND);
+ }
+ }
+
+ Paint textPaint;
+ Paint rectPaint;
+
+ public AprilTagCanvasAnnotator(Mat cameraMatrix)
+ {
+ this.cameraMatrix = cameraMatrix;
+
+ textPaint = new Paint();
+ textPaint.setColor(Color.WHITE);
+ textPaint.setAntiAlias(true);
+ textPaint.setTypeface(Typeface.DEFAULT_BOLD);
+
+ rectPaint = new Paint();
+ rectPaint.setAntiAlias(true);
+ rectPaint.setColor(Color.rgb(12, 145, 201));
+ rectPaint.setStyle(Paint.Style.FILL);
+ }
+
+ public void noteDrawParams(float bmpPxToCanvasPx, float canvasDensityScale)
+ {
+ if (bmpPxToCanvasPx != this.bmpPxToCanvasPx || canvasDensityScale != this.canvasDensityScale)
+ {
+ this.bmpPxToCanvasPx = bmpPxToCanvasPx;
+ this.canvasDensityScale = canvasDensityScale;
+
+ textPaint.setTextSize(40*canvasDensityScale);
+ }
+ }
+
+ /**
+ * Draw a 3D axis marker on a detection. (Similar to what Vuforia does)
+ *
+ * @param detection the detection to draw
+ * @param canvas the canvas to draw on
+ * @param tagsize size of the tag in SAME UNITS as pose
+ */
+ void drawAxisMarker(AprilTagDetection detection, Canvas canvas, double tagsize)
+ {
+ //Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsize, tagsize);
+ AprilTagProcessorImpl.Pose pose = AprilTagProcessorImpl.aprilTagPoseToOpenCvPose(detection.rawPose);
+
+ // in meters, actually.... will be mapped to screen coords
+ float axisLength = (float) (tagsize / 2.0);
+
+ // The points in 3D space we wish to project onto the 2D image plane.
+ // The origin of the coordinate space is assumed to be in the center of the detection.
+ MatOfPoint3f axis = new MatOfPoint3f(
+ new Point3(0,0,0),
+ new Point3(-axisLength,0,0),
+ new Point3(0,-axisLength,0),
+ new Point3(0,0,-axisLength)
+ );
+
+ // Project those points onto the image
+ MatOfPoint2f matProjectedPoints = new MatOfPoint2f();
+ Calib3d.projectPoints(axis, pose.rvec, pose.tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints);
+ Point[] projectedPoints = matProjectedPoints.toArray();
+
+ // The projection we did was good for the original resolution image, but now
+ // we need to scale those to their locations on the canvas.
+ for (Point p : projectedPoints)
+ {
+ p.x *= bmpPxToCanvasPx;
+ p.y *= bmpPxToCanvasPx;
+ }
+
+ // Use the 3D distance to the target, as well as the physical size of the
+ // target in the real world to scale the thickness of lines.
+ double dist3d = Math.sqrt(Math.pow(detection.rawPose.x, 2) + Math.pow(detection.rawPose.y, 2) + Math.pow(detection.rawPose.z, 2));
+ float axisThickness = (float) ((5 / dist3d) * (tagsize / 0.166) * bmpPxToCanvasPx); // looks about right I guess
+
+ redAxisPaint.setStrokeWidth(axisThickness);
+ greenAxisPaint.setStrokeWidth(axisThickness);
+ blueAxisPaint.setStrokeWidth(axisThickness);
+
+ // Now draw the axes
+ canvas.drawLine((float)projectedPoints[0].x,(float)projectedPoints[0].y, (float)projectedPoints[1].x, (float)projectedPoints[1].y, redAxisPaint);
+ canvas.drawLine((float)projectedPoints[0].x,(float)projectedPoints[0].y, (float)projectedPoints[2].x, (float)projectedPoints[2].y, greenAxisPaint);
+ canvas.drawLine((float)projectedPoints[0].x,(float)projectedPoints[0].y, (float)projectedPoints[3].x, (float)projectedPoints[3].y, blueAxisPaint);
+ }
+
+ /**
+ * Draw a 3D cube marker on a detection
+ *
+ * @param detection the detection to draw
+ * @param canvas the canvas to draw on
+ * @param tagsize size of the tag in SAME UNITS as pose
+ */
+ void draw3dCubeMarker(AprilTagDetection detection, Canvas canvas, double tagsize)
+ {
+ //Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsize, tagsize);
+ AprilTagProcessorImpl.Pose pose = AprilTagProcessorImpl.aprilTagPoseToOpenCvPose(detection.rawPose);
+
+ // The points in 3D space we wish to project onto the 2D image plane.
+ // The origin of the coordinate space is assumed to be in the center of the detection.
+ MatOfPoint3f axis = new MatOfPoint3f(
+ new Point3(-tagsize/2, tagsize/2,0),
+ new Point3( tagsize/2, tagsize/2,0),
+ new Point3( tagsize/2,-tagsize/2,0),
+ new Point3(-tagsize/2,-tagsize/2,0),
+ new Point3(-tagsize/2, tagsize/2,-tagsize),
+ new Point3( tagsize/2, tagsize/2,-tagsize),
+ new Point3( tagsize/2,-tagsize/2,-tagsize),
+ new Point3(-tagsize/2,-tagsize/2,-tagsize));
+
+ // Project those points
+ MatOfPoint2f matProjectedPoints = new MatOfPoint2f();
+ Calib3d.projectPoints(axis, pose.rvec, pose.tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints);
+ Point[] projectedPoints = matProjectedPoints.toArray();
+
+ // The projection we did was good for the original resolution image, but now
+ // we need to scale those to their locations on the canvas.
+ for (Point p : projectedPoints)
+ {
+ p.x *= bmpPxToCanvasPx;
+ p.y *= bmpPxToCanvasPx;
+ }
+
+ // Use the 3D distance to the target, as well as the physical size of the
+ // target in the real world to scale the thickness of lines.
+ double dist3d = Math.sqrt(Math.pow(detection.rawPose.x, 2) + Math.pow(detection.rawPose.y, 2) + Math.pow(detection.rawPose.z, 2));
+ float thickness = (float) ((3.5 / dist3d) * (tagsize / 0.166) * bmpPxToCanvasPx); // looks about right I guess
+
+ boxPillarPaint.setStrokeWidth(thickness);
+ boxTopPaint.setStrokeWidth(thickness);
+
+ float[] pillarPts = new float[16];
+
+ // Pillars
+ for(int i = 0; i < 4; i++)
+ {
+ pillarPts[i*4+0] = (float) projectedPoints[i].x;
+ pillarPts[i*4+1] = (float) projectedPoints[i].y;
+
+ pillarPts[i*4+2] = (float) projectedPoints[i+4].x;
+ pillarPts[i*4+3] = (float) projectedPoints[i+4].y;
+ }
+
+ canvas.drawLines(pillarPts, boxPillarPaint);
+
+ // Top lines
+ float[] topPts = new float[] {
+ (float) projectedPoints[4].x, (float) projectedPoints[4].y, (float) projectedPoints[5].x, (float) projectedPoints[5].y,
+ (float) projectedPoints[5].x, (float) projectedPoints[5].y, (float) projectedPoints[6].x, (float) projectedPoints[6].y,
+ (float) projectedPoints[6].x, (float) projectedPoints[6].y, (float) projectedPoints[7].x, (float) projectedPoints[7].y,
+ (float) projectedPoints[4].x, (float) projectedPoints[4].y, (float) projectedPoints[7].x, (float) projectedPoints[7].y
+ };
+
+ canvas.drawLines(topPts, boxTopPaint);
+ }
+
+ /**
+ * Draw an outline marker on the detection
+ * @param detection the detection to draw
+ * @param canvas the canvas to draw on
+ * @param tagsize size of the tag in SAME UNITS as pose
+ */
+ void drawOutlineMarker(AprilTagDetection detection, Canvas canvas, double tagsize)
+ {
+ // Use the 3D distance to the target, as well as the physical size of the
+ // target in the real world to scale the thickness of lines.
+ double dist3d = Math.sqrt(Math.pow(detection.rawPose.x, 2) + Math.pow(detection.rawPose.y, 2) + Math.pow(detection.rawPose.z, 2));
+ float axisThickness = (float) ((5 / dist3d) * (tagsize / 0.166) * bmpPxToCanvasPx); // looks about right I guess
+
+ redAxisPaint.setStrokeWidth(axisThickness);
+ greenAxisPaint.setStrokeWidth(axisThickness);
+ blueAxisPaint.setStrokeWidth(axisThickness);
+
+ canvas.drawLine(
+ (float)detection.corners[0].x*bmpPxToCanvasPx,(float)detection.corners[0].y*bmpPxToCanvasPx,
+ (float)detection.corners[1].x*bmpPxToCanvasPx, (float)detection.corners[1].y*bmpPxToCanvasPx,
+ redAxisPaint);
+
+ canvas.drawLine(
+ (float)detection.corners[1].x*bmpPxToCanvasPx,(float)detection.corners[1].y*bmpPxToCanvasPx,
+ (float)detection.corners[2].x*bmpPxToCanvasPx, (float)detection.corners[2].y*bmpPxToCanvasPx,
+ greenAxisPaint);
+
+ canvas.drawLine(
+ (float)detection.corners[0].x*bmpPxToCanvasPx,(float)detection.corners[0].y*bmpPxToCanvasPx,
+ (float)detection.corners[3].x*bmpPxToCanvasPx, (float)detection.corners[3].y*bmpPxToCanvasPx,
+ blueAxisPaint);
+
+ canvas.drawLine(
+ (float)detection.corners[2].x*bmpPxToCanvasPx,(float)detection.corners[2].y*bmpPxToCanvasPx,
+ (float)detection.corners[3].x*bmpPxToCanvasPx, (float)detection.corners[3].y*bmpPxToCanvasPx,
+ blueAxisPaint);
+ }
+
+ /**
+ * Draw the Tag's ID on the tag
+ * @param detection the detection to draw
+ * @param canvas the canvas to draw on
+ */
+ void drawTagID(AprilTagDetection detection, Canvas canvas)
+ {
+ float cornerRound = 5 * canvasDensityScale;
+
+ float tag_id_width = 120*canvasDensityScale;
+ float tag_id_height = 50*canvasDensityScale;
+
+ float id_x = (float) detection.center.x * bmpPxToCanvasPx - tag_id_width/2;
+ float id_y = (float) detection.center.y * bmpPxToCanvasPx - tag_id_height/2;
+
+ float tag_id_text_x = id_x + 10*canvasDensityScale;
+ float tag_id_text_y = id_y + 40*canvasDensityScale;
+
+ Point lowerLeft = detection.corners[0];
+ Point lowerRight = detection.corners[1];
+
+ canvas.save();
+ canvas.rotate((float) Math.toDegrees(Math.atan2(lowerRight.y - lowerLeft.y, lowerRight.x-lowerLeft.x)), (float) detection.center.x*bmpPxToCanvasPx, (float) detection.center.y*bmpPxToCanvasPx);
+
+ canvas.drawRoundRect(id_x, id_y, id_x+tag_id_width, id_y+tag_id_height, cornerRound, cornerRound, rectPaint);
+ canvas.drawText(String.format("ID %02d", detection.id), tag_id_text_x, tag_id_text_y, textPaint);
+
+ canvas.restore();
+ }
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java
new file mode 100644
index 00000000..e8c95e12
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagDetection.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import org.opencv.core.Point;
+
+public class AprilTagDetection
+{
+ /**
+ * The numerical ID of the detection
+ */
+ public int id;
+
+ /**
+ * The number of bits corrected when reading the tag ID payload
+ */
+ public int hamming;
+
+ /*
+ * How much margin remains before the detector would decide to reject a tag
+ */
+ public float decisionMargin;
+
+ /*
+ * The image pixel coordinates of the center of the tag
+ */
+ public Point center;
+
+ /*
+ * The image pixel coordinates of the corners of the tag
+ */
+ public Point[] corners;
+
+ /*
+ * Metadata known about this tag from the tag library set on the detector;
+ * will be NULL if the tag was not in the tag library
+ */
+ public AprilTagMetadata metadata;
+
+ /*
+ * 6DOF pose data formatted in useful ways for FTC gameplay
+ */
+ public AprilTagPoseFtc ftcPose;
+
+ /*
+ * Raw translation vector and orientation matrix returned by the pose solver
+ */
+ public AprilTagPoseRaw rawPose;
+
+ /*
+ * Timestamp of when the image in which this detection was found was acquired
+ */
+ public long frameAcquisitionNanoTime;
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java
new file mode 100644
index 00000000..44281c39
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagGameDatabase.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import org.firstinspires.ftc.robotcore.external.matrices.VectorF;
+import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.Quaternion;
+
+public class AprilTagGameDatabase
+{
+ /**
+ * Get the {@link AprilTagLibrary} for the current season game, plus sample tags
+ * @return the {@link AprilTagLibrary} for the current season game, plus sample tags
+ */
+ public static AprilTagLibrary getCurrentGameTagLibrary()
+ {
+ return new AprilTagLibrary.Builder()
+ .addTags(getSampleTagLibrary())
+ .addTags(getCenterStageTagLibrary())
+ .build();
+ }
+
+ /**
+ * Get the {@link AprilTagLibrary} for the Center Stage FTC game
+ * @return the {@link AprilTagLibrary} for the Center Stage FTC game
+ */
+ public static AprilTagLibrary getCenterStageTagLibrary()
+ {
+ return new AprilTagLibrary.Builder()
+ .addTag(0, "MEOW",
+ 0.166, new VectorF(0,0,0), DistanceUnit.METER,
+ Quaternion.identityQuaternion())
+ .addTag(1, "WOOF",
+ 0.322, new VectorF(0,0,0), DistanceUnit.METER,
+ Quaternion.identityQuaternion())
+ .addTag(2, "OINK",
+ 0.166, new VectorF(0,0,0), DistanceUnit.METER,
+ Quaternion.identityQuaternion())
+ .build();
+ }
+
+ /**
+ * Get the {@link AprilTagLibrary} for the tags used in the sample OpModes
+ * @return the {@link AprilTagLibrary} for the tags used in the sample OpModes
+ */
+ public static AprilTagLibrary getSampleTagLibrary()
+ {
+ return new AprilTagLibrary.Builder()
+ .addTag(583, "Nemo",
+ 4, DistanceUnit.INCH)
+ .addTag(584, "Jonah",
+ 4, DistanceUnit.INCH)
+ .addTag(585, "Cousteau",
+ 6, DistanceUnit.INCH)
+ .addTag(586, "Ariel",
+ 6, DistanceUnit.INCH)
+ .build();
+ }
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java
new file mode 100644
index 00000000..f30f3b3d
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagLibrary.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import org.firstinspires.ftc.robotcore.external.matrices.VectorF;
+import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.Quaternion;
+
+import java.util.ArrayList;
+
+/**
+ * A tag library contains metadata about tags such as
+ * their ID, name, size, and 6DOF position on the field
+ */
+public class AprilTagLibrary
+{
+ private final AprilTagMetadata[] data;
+
+ private AprilTagLibrary(AprilTagMetadata[] data)
+ {
+ this.data = data;
+ }
+
+ /**
+ * Get the metadata of all tags in this library
+ * @return the metadata of all tags in this library
+ */
+ public AprilTagMetadata[] getAllTags()
+ {
+ return data;
+ }
+
+ /**
+ * Get the metadata for a specific tag in this library
+ * @param id the ID of the tag in question
+ * @return either {@link AprilTagMetadata} for the tag, or
+ * NULL if it isn't in this library
+ */
+ public AprilTagMetadata lookupTag(int id)
+ {
+ for (AprilTagMetadata tagMetadata : data)
+ {
+ if (tagMetadata.id == id)
+ {
+ return tagMetadata;
+ }
+ }
+
+ return null;
+ }
+
+ public static class Builder
+ {
+ private ArrayList data = new ArrayList<>();
+ private boolean allowOverwrite = false;
+
+ /**
+ * Set whether to allow overwriting an existing entry in the tag
+ * library with a new entry of the same ID
+ * @param allowOverwrite whether to allow overwrite
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setAllowOverwrite(boolean allowOverwrite)
+ {
+ this.allowOverwrite = allowOverwrite;
+ return this;
+ }
+
+ /**
+ * Add a tag to this tag library
+ * @param aprilTagMetadata the tag to add
+ * @return the {@link Builder} object, to allow for method chaining
+ * @throws RuntimeException if trying to add a tag that already exists
+ * in this library, unless you called {@link #setAllowOverwrite(boolean)}
+ */
+ public Builder addTag(AprilTagMetadata aprilTagMetadata)
+ {
+ for (AprilTagMetadata m : data)
+ {
+ if (m.id == aprilTagMetadata.id)
+ {
+ if (allowOverwrite)
+ {
+ // This is ONLY safe bc we immediately stop iteration here
+ data.remove(m);
+ break;
+ }
+ else
+ {
+ throw new RuntimeException("You attempted to add a tag to the library when it already contains a tag with that ID. You can call .setAllowOverwrite(true) to allow overwriting the existing entry");
+ }
+ }
+ }
+
+ data.add(aprilTagMetadata);
+ return this;
+ }
+
+ /**
+ * Add a tag to this tag library
+ * @param id the ID of the tag
+ * @param name a text name for the tag
+ * @param size the physical size of the tag in the real world (measured black edge to black edge)
+ * @param fieldPosition a vector describing the tag's 3d translation on the field
+ * @param distanceUnit the units used for size and fieldPosition
+ * @param fieldOrientation a quaternion describing the tag's orientation on the field
+ * @return the {@link Builder} object, to allow for method chaining
+ * @throws RuntimeException if trying to add a tag that already exists
+ * in this library, unless you called {@link #setAllowOverwrite(boolean)}
+ */
+ public Builder addTag(int id, String name, double size, VectorF fieldPosition, DistanceUnit distanceUnit, Quaternion fieldOrientation)
+ {
+ return addTag(new AprilTagMetadata(id, name, size, fieldPosition, distanceUnit, fieldOrientation));
+ }
+
+ /**
+ * Add a tag to this tag library
+ * @param id the ID of the tag
+ * @param name a text name for the tag
+ * @param size the physical size of the tag in the real world (measured black edge to black edge)
+ * @param distanceUnit the units used for size and fieldPosition
+ * @return the {@link Builder} object, to allow for method chaining
+ * @throws RuntimeException if trying to add a tag that already exists
+ * in this library, unless you called {@link #setAllowOverwrite(boolean)}
+ */
+ public Builder addTag(int id, String name, double size, DistanceUnit distanceUnit)
+ {
+ return addTag(new AprilTagMetadata(id, name, size, new VectorF(0,0,0), distanceUnit, Quaternion.identityQuaternion()));
+ }
+
+ /**
+ * Add multiple tags to this tag library
+ * @param library an existing tag library to add to this one
+ * @return the {@link Builder} object, to allow for method chaining
+ * @throws RuntimeException if trying to add a tag that already exists
+ * in this library, unless you called {@link #setAllowOverwrite(boolean)}
+ */
+ public Builder addTags(AprilTagLibrary library)
+ {
+ for (AprilTagMetadata m : library.getAllTags())
+ {
+ // Delegate to this implementation so we get duplicate checking for free
+ addTag(m);
+ }
+ return this;
+ }
+
+ /**
+ * Create an {@link AprilTagLibrary} object from the specified tags
+ * @return an {@link AprilTagLibrary} object
+ */
+ public AprilTagLibrary build()
+ {
+ return new AprilTagLibrary(data.toArray(new AprilTagMetadata[0]));
+ }
+ }
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java
new file mode 100644
index 00000000..40762e99
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagMetadata.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import org.firstinspires.ftc.robotcore.external.matrices.VectorF;
+import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.Quaternion;
+
+public class AprilTagMetadata
+{
+ public final int id;
+ public final double tagsize;
+ public final String name;
+ public final DistanceUnit distanceUnit;
+ public final VectorF fieldPosition;
+ public final Quaternion fieldOrientation;
+
+ /**
+ * Add a tag to this tag library
+ * @param id the ID of the tag
+ * @param name a text name for the tag
+ * @param tagsize the physical size of the tag in the real world (measured black edge to black edge)
+ * @param fieldPosition a vector describing the tag's 3d translation on the field
+ * @param distanceUnit the units used for size and fieldPosition
+ * @param fieldOrientation a quaternion describing the tag's orientation on the field
+ */
+ public AprilTagMetadata(int id, String name, double tagsize, VectorF fieldPosition, DistanceUnit distanceUnit, Quaternion fieldOrientation)
+ {
+ this.id = id;
+ this.name = name;
+ this.tagsize = tagsize;
+ this.fieldOrientation = fieldOrientation;
+ this.fieldPosition = fieldPosition;
+ this.distanceUnit = distanceUnit;
+ }
+
+ /**
+ * Add a tag to this tag library
+ * @param id the ID of the tag
+ * @param name a text name for the tag
+ * @param tagsize the physical size of the tag in the real world (measured black edge to black edge)
+ * @param distanceUnit the units used for size and fieldPosition
+ */
+ public AprilTagMetadata(int id, String name, double tagsize, DistanceUnit distanceUnit)
+ {
+ this.id = id;
+ this.name = name;
+ this.tagsize = tagsize;
+ this.fieldOrientation = Quaternion.identityQuaternion();
+ this.fieldPosition = new VectorF(0,0,0);
+ this.distanceUnit = distanceUnit;
+ }
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java
new file mode 100644
index 00000000..16300f7f
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseFtc.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+/**
+ * AprilTagPoseFtc represents the AprilTag's position in space, relative to the camera.
+ * It is a realignment of the raw AprilTag Pose to be consistent with a forward-looking camera on an FTC robot.
+ * Also includes additional derived values to simplify driving towards any Tag.
+ *
+ * Note: These member definitions describe the camera Lens as the reference point for axis measurements.
+ * This measurement may be off by the distance from the lens to the image sensor itself. For most webcams this is a reasonable approximation.
+ *
+ * @see apriltag-detection-values.pdf
+ */
+public class AprilTagPoseFtc
+{
+
+ /**
+ * X translation of AprilTag, relative to camera lens. Measured sideways (Horizontally in camera image) the positive X axis extends out to the right of the camera viewpoint.
+ * An x value of zero implies that the Tag is centered between the left and right sides of the Camera image.
+ */
+ public double x;
+
+ /**
+ * Y translation of AprilTag, relative to camera lens. Measured forwards (Horizontally in camera image) the positive Y axis extends out in the direction the camera is pointing.
+ * A y value of zero implies that the Tag is touching (aligned with) the lens of the camera, which is physically unlikley. This value should always be positive.
+ */
+ public double y;
+
+ /**
+ * Z translation of AprilTag, relative to camera lens. Measured upwards (Vertically in camera image) the positive Z axis extends Upwards in the camera viewpoint.
+ * A z value of zero implies that the Tag is centered between the top and bottom of the camera image.
+ */
+ public double z;
+
+ /**
+ * Rotation of AprilTag around the Z axis. Right-Hand-Rule defines positive Yaw rotation as Counter-Clockwise when viewed from above.
+ * A yaw value of zero implies that the camera is directly in front of the Tag, as viewed from above.
+ */
+ public double yaw;
+
+ /**
+ * Rotation of AprilTag around the X axis. Right-Hand-Rule defines positive Pitch rotation as the Tag Image face twisting down when viewed from the camera.
+ * A pitch value of zero implies that the camera is directly in front of the Tag, as viewed from the side.
+ */
+ public double pitch;
+
+ /**
+ * Rotation of AprilTag around the Y axis. Right-Hand-Rule defines positive Roll rotation as the Tag Image rotating Clockwise when viewed from the camera.
+ * A roll value of zero implies that the Tag image is alligned squarely and upright, when viewed in the camera image frame.
+ */
+ public double roll;
+
+ /**
+ * Range, (Distance), from the Camera lens to the center of the Tag, as measured along the X-Y plane (across the ground).
+ */
+ public double range;
+
+ /**
+ * Bearing, or Horizontal Angle, from the "camera center-line", to the "line joining the Camera lens and the Center of the Tag".
+ * This angle is measured across the X-Y plane (across the ground).
+ * A positive Bearing indicates that the robot must employ a positive Yaw (rotate counter clockwise) in order to point towards the target.
+ */
+ public double bearing;
+
+ /**
+ * Elevation, (Vertical Angle), from "the camera center-line", to "the line joining the Camera Lens and the Center of the Tag".
+ * A positive Elevation indicates that the robot must employ a positive Pitch (tilt up) in order to point towards the target.
+ */
+ public double elevation;
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java
new file mode 100644
index 00000000..edd67664
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagPoseRaw.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import org.firstinspires.ftc.robotcore.external.matrices.MatrixF;
+
+public class AprilTagPoseRaw
+{
+ /**
+ * X translation
+ */
+ public double x;
+
+ /**
+ * Y translation
+ */
+ public double y;
+
+ /**
+ * Z translation
+ */
+ public double z;
+
+ /**
+ * 3x3 rotation matrix
+ */
+ public MatrixF R;
+}
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java
new file mode 100644
index 00000000..a7a0737f
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessor.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
+import org.firstinspires.ftc.vision.VisionProcessor;
+import org.opencv.calib3d.Calib3d;
+import org.openftc.apriltag.AprilTagDetectorJNI;
+
+import java.util.ArrayList;
+
+public abstract class AprilTagProcessor implements VisionProcessor
+{
+ public static final int THREADS_DEFAULT = 3;
+
+ public enum TagFamily
+ {
+ TAG_36h11(AprilTagDetectorJNI.TagFamily.TAG_36h11),
+ TAG_25h9(AprilTagDetectorJNI.TagFamily.TAG_25h9),
+ TAG_16h5(AprilTagDetectorJNI.TagFamily.TAG_16h5),
+ TAG_standard41h12(AprilTagDetectorJNI.TagFamily.TAG_standard41h12);
+
+ final AprilTagDetectorJNI.TagFamily ATLibTF;
+
+ TagFamily(AprilTagDetectorJNI.TagFamily ATLibTF)
+ {
+ this.ATLibTF = ATLibTF;
+ }
+ }
+
+ public static AprilTagProcessor easyCreateWithDefaults()
+ {
+ return new AprilTagProcessor.Builder().build();
+ }
+
+ public static class Builder
+ {
+ private double fx, fy, cx, cy;
+ private TagFamily tagFamily = TagFamily.TAG_36h11;
+ private AprilTagLibrary tagLibrary = AprilTagGameDatabase.getCurrentGameTagLibrary();
+ private DistanceUnit outputUnitsLength = DistanceUnit.INCH;
+ private AngleUnit outputUnitsAngle = AngleUnit.DEGREES;
+ private int threads = THREADS_DEFAULT;
+
+ private boolean drawAxes = false;
+ private boolean drawCube = false;
+ private boolean drawOutline = true;
+ private boolean drawTagId = true;
+
+ /**
+ * Set the camera calibration parameters (needed for accurate 6DOF pose unless the
+ * SDK has a built in calibration for your camera)
+ * @param fx see opencv 8 parameter camera model
+ * @param fy see opencv 8 parameter camera model
+ * @param cx see opencv 8 parameter camera model
+ * @param cy see opencv 8 parameter camera model
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setLensIntrinsics(double fx, double fy, double cx, double cy)
+ {
+ this.fx = fx;
+ this.fy = fy;
+ this.cx = cx;
+ this.cy = cy;
+ return this;
+ }
+
+ /**
+ * Set the tag family this detector will be used to detect (it can only be used
+ * for one tag family at a time)
+ * @param tagFamily the tag family this detector will be used to detect
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setTagFamily(TagFamily tagFamily)
+ {
+ this.tagFamily = tagFamily;
+ return this;
+ }
+
+ /**
+ * Inform the detector about known tags. The tag library is used to allow solving
+ * for 6DOF pose, based on the physical size of the tag. Tags which are not in the
+ * library will not have their pose solved for
+ * @param tagLibrary a library of known tags for the detector to use when trying to solve pose
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setTagLibrary(AprilTagLibrary tagLibrary)
+ {
+ this.tagLibrary = tagLibrary;
+ return this;
+ }
+
+ /**
+ * Set the units you want translation and rotation data provided in, inside any
+ * {@link AprilTagPoseRaw} or {@link AprilTagPoseFtc} objects
+ * @param distanceUnit translational units
+ * @param angleUnit rotational units
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setOutputUnits(DistanceUnit distanceUnit, AngleUnit angleUnit)
+ {
+ this.outputUnitsLength = distanceUnit;
+ this.outputUnitsAngle = angleUnit;
+ return this;
+ }
+
+ /**
+ * Set whether to draw a 3D crosshair on the tag (what Vuforia did)
+ * @param drawAxes whether to draw it
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setDrawAxes(boolean drawAxes)
+ {
+ this.drawAxes = drawAxes;
+ return this;
+ }
+
+ /**
+ * Set whether to draw a 3D cube projecting from the tag
+ * @param drawCube whether to draw it lol
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setDrawCubeProjection(boolean drawCube)
+ {
+ this.drawCube = drawCube;
+ return this;
+ }
+
+ /**
+ * Set whether to draw a 2D outline around the tag detection
+ * @param drawOutline whether to draw it
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setDrawTagOutline(boolean drawOutline)
+ {
+ this.drawOutline = drawOutline;
+ return this;
+ }
+
+ /**
+ * Set whether to annotate the tag detection with its ID
+ * @param drawTagId whether to annotate the tag with its ID
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setDrawTagID(boolean drawTagId)
+ {
+ this.drawTagId = drawTagId;
+ return this;
+ }
+
+ /**
+ * Set the number of threads the tag detector should use
+ * @param threads the number of threads the tag detector should use
+ * @return the {@link Builder} object, to allow for method chaining
+ */
+ public Builder setNumThreads(int threads)
+ {
+ this.threads = threads;
+ return this;
+ }
+
+ /**
+ * Create a {@link VisionProcessor} object which may be attached to
+ * a {link org.firstinspires.ftc.vision.VisionPortal} using
+ * {link org.firstinspires.ftc.vision.VisionPortal.Builder#addProcessor(VisionProcessor)}
+ * @return a {@link VisionProcessor} object
+ */
+ public AprilTagProcessor build()
+ {
+ if (tagLibrary == null)
+ {
+ throw new RuntimeException("Cannot create AprilTagProcessor without setting tag library!");
+ }
+
+ if (tagFamily == null)
+ {
+ throw new RuntimeException("Cannot create AprilTagProcessor without setting tag family!");
+ }
+
+ return new AprilTagProcessorImpl(
+ fx, fy, cx, cy,
+ outputUnitsLength, outputUnitsAngle, tagLibrary,
+ drawAxes, drawCube, drawOutline, drawTagId,
+ tagFamily, threads
+ );
+ }
+ }
+
+ /**
+ * Set the detector decimation
+ *
+ * Higher decimation increases frame rate at the expense of reduced range
+ *
+ * @param decimation detector decimation
+ */
+ public abstract void setDecimation(float decimation);
+
+ public enum PoseSolver
+ {
+ APRILTAG_BUILTIN(-1),
+ OPENCV_ITERATIVE(Calib3d.SOLVEPNP_ITERATIVE),
+ OPENCV_SOLVEPNP_EPNP(Calib3d.SOLVEPNP_EPNP),
+ OPENCV_IPPE(Calib3d.SOLVEPNP_IPPE),
+ OPENCV_IPPE_SQUARE(Calib3d.SOLVEPNP_IPPE_SQUARE),
+ OPENCV_SQPNP(Calib3d.SOLVEPNP_SQPNP);
+
+ final int code;
+
+ PoseSolver(int code)
+ {
+ this.code = code;
+ }
+ }
+
+ /**
+ * Specify the method used to calculate 6DOF pose from the tag corner positions once
+ * found by the AprilTag algorithm
+ * @param poseSolver the pose solver to use
+ */
+ public abstract void setPoseSolver(PoseSolver poseSolver);
+
+ /**
+ * Get the average time in milliseconds the currently set pose solver is taking
+ * to converge on a solution PER TAG. Some pose solvers are much more expensive
+ * than others...
+ * @return average time to converge on a solution per tag in milliseconds
+ */
+ public abstract int getPerTagAvgPoseSolveTime();
+
+ /**
+ * Get a list containing the latest detections, which may be stale
+ * i.e. the same as the last time you called this
+ * @return a list containing the latest detections.
+ */
+ public abstract ArrayList getDetections();
+
+ /**
+ * Get a list containing detections that were detected SINCE THE PREVIOUS CALL to this method,
+ * or NULL if no new detections are available. This is useful to avoid re-processing the same
+ * detections multiple times.
+ * @return a list containing fresh detections, or NULL.
+ */
+ public abstract ArrayList getFreshDetections();
+}
+
diff --git a/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java
new file mode 100644
index 00000000..047538ae
--- /dev/null
+++ b/Vision/src/main/java/org/firstinspires/ftc/vision/apriltag/AprilTagProcessorImpl.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (c) 2023 FIRST
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted (subject to the limitations in the disclaimer below) provided that
+ * the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list
+ * of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this
+ * list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ *
+ * Neither the name of FIRST nor the names of its contributors may be used to
+ * endorse or promote products derived from this software without specific prior
+ * written permission.
+ *
+ * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
+ * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.firstinspires.ftc.vision.apriltag;
+
+import android.graphics.Canvas;
+
+import com.qualcomm.robotcore.eventloop.opmode.Disabled;
+import com.qualcomm.robotcore.util.MovingStatistics;
+
+import org.firstinspires.ftc.robotcore.external.matrices.GeneralMatrixF;
+import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder;
+import org.firstinspires.ftc.robotcore.external.navigation.AxesReference;
+import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
+import org.firstinspires.ftc.robotcore.external.navigation.Orientation;
+import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration;
+import org.opencv.calib3d.Calib3d;
+import org.opencv.core.CvType;
+import org.opencv.core.Mat;
+import org.opencv.core.MatOfDouble;
+import org.opencv.core.MatOfPoint2f;
+import org.opencv.core.MatOfPoint3f;
+import org.opencv.core.Point;
+import org.opencv.core.Point3;
+import org.opencv.imgproc.Imgproc;
+import org.openftc.apriltag.AprilTagDetectorJNI;
+import org.openftc.apriltag.ApriltagDetectionJNI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+
+@Disabled
+public class AprilTagProcessorImpl extends AprilTagProcessor
+{
+ public static final String TAG = "AprilTagProcessorImpl";
+ Logger logger = LoggerFactory.getLogger(TAG);
+ private long nativeApriltagPtr;
+ private Mat grey = new Mat();
+ private ArrayList detections = new ArrayList<>();
+
+ private ArrayList detectionsUpdate = new ArrayList<>();
+ private final Object detectionsUpdateSync = new Object();
+ private boolean drawAxes;
+ private boolean drawCube;
+ private boolean drawOutline;
+ private boolean drawTagID;
+
+ private Mat cameraMatrix;
+
+ private double fx;
+ private double fy;
+ private double cx;
+ private double cy;
+
+ private final AprilTagLibrary tagLibrary;
+
+ private float decimation;
+ private boolean needToSetDecimation;
+ private final Object decimationSync = new Object();
+
+ private AprilTagCanvasAnnotator canvasAnnotator;
+
+ private final DistanceUnit outputUnitsLength;
+ private final AngleUnit outputUnitsAngle;
+
+ private volatile PoseSolver poseSolver = PoseSolver.OPENCV_ITERATIVE;
+
+ public AprilTagProcessorImpl(double fx, double fy, double cx, double cy, DistanceUnit outputUnitsLength, AngleUnit outputUnitsAngle, AprilTagLibrary tagLibrary, boolean drawAxes, boolean drawCube, boolean drawOutline, boolean drawTagID, TagFamily tagFamily, int threads)
+ {
+ this.fx = fx;
+ this.fy = fy;
+ this.cx = cx;
+ this.cy = cy;
+
+ this.tagLibrary = tagLibrary;
+ this.outputUnitsLength = outputUnitsLength;
+ this.outputUnitsAngle = outputUnitsAngle;
+ this.drawAxes = drawAxes;
+ this.drawCube = drawCube;
+ this.drawOutline = drawOutline;
+ this.drawTagID = drawTagID;
+
+ // Allocate a native context object. See the corresponding deletion in the finalizer
+ nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(tagFamily.ATLibTF.string, 3, threads);
+ }
+
+ @Override
+ protected void finalize()
+ {
+ // Might be null if createApriltagDetector() threw an exception
+ if(nativeApriltagPtr != 0)
+ {
+ // Delete the native context we created in the constructor
+ AprilTagDetectorJNI.releaseApriltagDetector(nativeApriltagPtr);
+ nativeApriltagPtr = 0;
+ }
+ else
+ {
+ logger.warn("AprilTagDetectionPipeline.finalize(): nativeApriltagPtr was NULL");
+ }
+ }
+
+ @Override
+ public void init(int width, int height, CameraCalibration calibration)
+ {
+ // If the user didn't give us a calibration, but we have one built in,
+ // then go ahead and use it!!
+ if (calibration != null && fx == 0 && fy == 0 && cx == 0 && cy == 0
+ && !(calibration.focalLengthX == 0 && calibration.focalLengthY == 0 && calibration.principalPointX == 0 && calibration.principalPointY == 0)) // needed because we may get an all zero calibration to indicate none, instead of null
+ {
+ fx = calibration.focalLengthX;
+ fy = calibration.focalLengthY;
+ cx = calibration.principalPointX;
+ cy = calibration.principalPointY;
+
+ logger.warn(String.format("User did not provide a camera calibration; but we DO have a built in calibration we can use.\n [%dx%d] (may be scaled) %s\nfx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f",
+ calibration.getSize().getWidth(), calibration.getSize().getHeight(), calibration.getIdentity().toString(), fx, fy, cx, cy));
+ }
+ else if (fx == 0 && fy == 0 && cx == 0 && cy == 0)
+ {
+ // set it to *something* so we don't crash the native code
+
+ String warning = "User did not provide a camera calibration, nor was a built-in calibration found for this camera; 6DOF pose data will likely be inaccurate.";
+ logger.warn(warning);
+ // RobotLog.addGlobalWarningMessage(warning);
+
+ fx = 578.272;
+ fy = 578.272;
+ cx = (double) width /2;
+ cy = (double) height /2;
+ }
+ else
+ {
+ logger.warn(String.format("User provided their own camera calibration fx=%7.3f fy=%7.3f cx=%7.3f cy=%7.3f",
+ fx, fy, cx, cy));
+ }
+
+ constructMatrix();
+
+ canvasAnnotator = new AprilTagCanvasAnnotator(cameraMatrix);
+ }
+
+ @Override
+ public Object processFrame(Mat input, long captureTimeNanos)
+ {
+ // Convert to greyscale
+ Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY);
+
+ synchronized (decimationSync)
+ {
+ if(needToSetDecimation)
+ {
+ AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation);
+ needToSetDecimation = false;
+ }
+ }
+
+ // Run AprilTag
+ detections = runAprilTagDetectorForMultipleTagSizes(captureTimeNanos);
+
+ synchronized (detectionsUpdateSync)
+ {
+ detectionsUpdate = detections;
+ }
+
+ // TODO do we need to deep copy this so the user can't mess with it before use in onDrawFrame()?
+ return detections;
+ }
+
+ private MovingStatistics solveTime = new MovingStatistics(50);
+
+ // We cannot use runAprilTagDetectorSimple because we cannot assume tags are all the same size
+ ArrayList runAprilTagDetectorForMultipleTagSizes(long captureTimeNanos)
+ {
+ long ptrDetectionArray = AprilTagDetectorJNI.runApriltagDetector(nativeApriltagPtr, grey.dataAddr(), grey.width(), grey.height());
+ if (ptrDetectionArray != 0)
+ {
+ long[] detectionPointers = ApriltagDetectionJNI.getDetectionPointers(ptrDetectionArray);
+ ArrayList detections = new ArrayList<>(detectionPointers.length);
+
+ for (long ptrDetection : detectionPointers)
+ {
+ AprilTagDetection detection = new AprilTagDetection();
+ detection.frameAcquisitionNanoTime = captureTimeNanos;
+
+ detection.id = ApriltagDetectionJNI.getId(ptrDetection);
+
+ AprilTagMetadata metadata = tagLibrary.lookupTag(detection.id);
+ detection.metadata = metadata;
+
+ detection.hamming = ApriltagDetectionJNI.getHamming(ptrDetection);
+ detection.decisionMargin = ApriltagDetectionJNI.getDecisionMargin(ptrDetection);
+ double[] center = ApriltagDetectionJNI.getCenterpoint(ptrDetection);
+ detection.center = new Point(center[0], center[1]);
+ double[][] corners = ApriltagDetectionJNI.getCorners(ptrDetection);
+
+ detection.corners = new Point[4];
+ for (int p = 0; p < 4; p++)
+ {
+ detection.corners[p] = new Point(corners[p][0], corners[p][1]);
+ }
+
+ if (metadata != null)
+ {
+ PoseSolver solver = poseSolver; // snapshot, can change
+
+ detection.rawPose = new AprilTagPoseRaw();
+
+ long startSolveTime = System.currentTimeMillis();
+
+ if (solver == PoseSolver.APRILTAG_BUILTIN)
+ {
+ // Translation
+ double[] pose = ApriltagDetectionJNI.getPoseEstimate(
+ ptrDetection,
+ outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize),
+ fx, fy, cx, cy);
+
+ detection.rawPose.x = pose[0];
+ detection.rawPose.y = pose[1];
+ detection.rawPose.z = pose[2];
+
+ // Rotation
+ float[] rotMtxVals = new float[3 * 3];
+ for (int i = 0; i < 9; i++)
+ {
+ rotMtxVals[i] = (float) pose[3 + i];
+ }
+ detection.rawPose.R = new GeneralMatrixF(3, 3, rotMtxVals);
+ }
+ else
+ {
+ Pose opencvPose = poseFromTrapezoid(
+ detection.corners,
+ cameraMatrix,
+ outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize),
+ solver.code);
+
+ detection.rawPose.x = opencvPose.tvec.get(0,0)[0];
+ detection.rawPose.y = opencvPose.tvec.get(1,0)[0];
+ detection.rawPose.z = opencvPose.tvec.get(2,0)[0];
+
+ Mat R = new Mat(3, 3, CvType.CV_32F);
+ Calib3d.Rodrigues(opencvPose.rvec, R);
+
+ float[] tmp2 = new float[9];
+ R.get(0,0, tmp2);
+ detection.rawPose.R = new GeneralMatrixF(3,3, tmp2);
+ }
+
+ long endSolveTime = System.currentTimeMillis();
+ solveTime.add(endSolveTime-startSolveTime);
+ }
+ else
+ {
+ // We don't know anything about the tag size so we can't solve the pose
+ detection.rawPose = null;
+ }
+
+ if (detection.rawPose != null)
+ {
+ detection.ftcPose = new AprilTagPoseFtc();
+
+ detection.ftcPose.x = detection.rawPose.x;
+ detection.ftcPose.y = detection.rawPose.z;
+ detection.ftcPose.z = -detection.rawPose.y;
+
+ Orientation rot = Orientation.getOrientation(detection.rawPose.R, AxesReference.INTRINSIC, AxesOrder.YXZ, outputUnitsAngle);
+ detection.ftcPose.yaw = -rot.firstAngle;
+ detection.ftcPose.roll = rot.thirdAngle;
+ detection.ftcPose.pitch = rot.secondAngle;
+
+ detection.ftcPose.range = Math.hypot(detection.ftcPose.x, detection.ftcPose.y);
+ detection.ftcPose.bearing = outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(-detection.ftcPose.x, detection.ftcPose.y));
+ detection.ftcPose.elevation = outputUnitsAngle.fromUnit(AngleUnit.RADIANS, Math.atan2(detection.ftcPose.z, detection.ftcPose.y));
+ }
+
+ detections.add(detection);
+ }
+
+ ApriltagDetectionJNI.freeDetectionList(ptrDetectionArray);
+ return detections;
+ }
+
+ return new ArrayList<>();
+ }
+
+ private final Object drawSync = new Object();
+
+ @Override
+ public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext)
+ {
+ // Only one draw operation at a time thank you very much.
+ // (we could be called from two different threads - viewport or camera stream)
+ synchronized (drawSync)
+ {
+ if ((drawAxes || drawCube || drawOutline || drawTagID) && userContext != null)
+ {
+ canvasAnnotator.noteDrawParams(scaleBmpPxToCanvasPx, scaleCanvasDensity);
+
+ ArrayList dets = (ArrayList) userContext;
+
+ // For fun, draw 6DOF markers on the image.
+ for(AprilTagDetection detection : dets)
+ {
+ if (drawTagID)
+ {
+ canvasAnnotator.drawTagID(detection, canvas);
+ }
+
+ // Could be null if we couldn't solve the pose earlier due to not knowing tag size
+ if (detection.rawPose != null)
+ {
+ AprilTagMetadata metadata = tagLibrary.lookupTag(detection.id);
+ double tagSize = outputUnitsLength.fromUnit(metadata.distanceUnit, metadata.tagsize);
+
+ if (drawOutline)
+ {
+ canvasAnnotator.drawOutlineMarker(detection, canvas, tagSize);
+ }
+ if (drawAxes)
+ {
+ canvasAnnotator.drawAxisMarker(detection, canvas, tagSize);
+ }
+ if (drawCube)
+ {
+ canvasAnnotator.draw3dCubeMarker(detection, canvas, tagSize);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void setDecimation(float decimation)
+ {
+ synchronized (decimationSync)
+ {
+ this.decimation = decimation;
+ needToSetDecimation = true;
+ }
+ }
+
+ @Override
+ public void setPoseSolver(PoseSolver poseSolver)
+ {
+ this.poseSolver = poseSolver;
+ }
+
+ @Override
+ public int getPerTagAvgPoseSolveTime()
+ {
+ return (int) Math.round(solveTime.getMean());
+ }
+
+ public ArrayList getDetections()
+ {
+ return detections;
+ }
+
+ public ArrayList getFreshDetections()
+ {
+ synchronized (detectionsUpdateSync)
+ {
+ ArrayList ret = detectionsUpdate;
+ detectionsUpdate = null;
+ return ret;
+ }
+ }
+
+ void constructMatrix()
+ {
+ // Construct the camera matrix.
+ //
+ // -- --
+ // | fx 0 cx |
+ // | 0 fy cy |
+ // | 0 0 1 |
+ // -- --
+ //
+
+ cameraMatrix = new Mat(3,3, CvType.CV_32FC1);
+
+ cameraMatrix.put(0,0, fx);
+ cameraMatrix.put(0,1,0);
+ cameraMatrix.put(0,2, cx);
+
+ cameraMatrix.put(1,0,0);
+ cameraMatrix.put(1,1,fy);
+ cameraMatrix.put(1,2,cy);
+
+ cameraMatrix.put(2, 0, 0);
+ cameraMatrix.put(2,1,0);
+ cameraMatrix.put(2,2,1);
+ }
+
+ /**
+ * Converts an AprilTag pose to an OpenCV pose
+ * @param aprilTagPose pose to convert
+ * @return OpenCV output pose
+ */
+ static Pose aprilTagPoseToOpenCvPose(AprilTagPoseRaw aprilTagPose)
+ {
+ Pose pose = new Pose();
+ pose.tvec.put(0,0, aprilTagPose.x);
+ pose.tvec.put(1,0, aprilTagPose.y);
+ pose.tvec.put(2,0, aprilTagPose.z);
+
+ Mat R = new Mat(3, 3, CvType.CV_32F);
+
+ for (int i = 0; i < 3; i++)
+ {
+ for (int j = 0; j < 3; j++)
+ {
+ R.put(i,j, aprilTagPose.R.get(i,j));
+ }
+ }
+
+ Calib3d.Rodrigues(R, pose.rvec);
+
+ return pose;
+ }
+
+ /**
+ * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the
+ * original size of the tag.
+ *
+ * @param points the points which form the trapezoid
+ * @param cameraMatrix the camera intrinsics matrix
+ * @param tagsize the original length of the tag
+ * @return the 6DOF pose of the camera relative to the tag
+ */
+ static Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsize, int solveMethod)
+ {
+ // The actual 2d points of the tag detected in the image
+ MatOfPoint2f points2d = new MatOfPoint2f(points);
+
+ // The 3d points of the tag in an 'ideal projection'
+ Point3[] arrayPoints3d = new Point3[4];
+ arrayPoints3d[0] = new Point3(-tagsize/2, tagsize/2, 0);
+ arrayPoints3d[1] = new Point3(tagsize/2, tagsize/2, 0);
+ arrayPoints3d[2] = new Point3(tagsize/2, -tagsize/2, 0);
+ arrayPoints3d[3] = new Point3(-tagsize/2, -tagsize/2, 0);
+ MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d);
+
+ // Using this information, actually solve for pose
+ Pose pose = new Pose();
+ Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false, solveMethod);
+
+ return pose;
+ }
+
+ /*
+ * A simple container to hold both rotation and translation
+ * vectors, which together form a 6DOF pose.
+ */
+ static class Pose
+ {
+ Mat rvec;
+ Mat tvec;
+
+ public Pose()
+ {
+ rvec = new Mat(3, 1, CvType.CV_32F);
+ tvec = new Mat(3, 1, CvType.CV_32F);
+ }
+
+ public Pose(Mat rvec, Mat tvec)
+ {
+ this.rvec = rvec;
+ this.tvec = tvec;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/opencv/android/Utils.java b/Vision/src/main/java/org/opencv/android/Utils.java
new file mode 100644
index 00000000..38b99f08
--- /dev/null
+++ b/Vision/src/main/java/org/opencv/android/Utils.java
@@ -0,0 +1,142 @@
+package org.opencv.android;
+
+import android.graphics.Bitmap;
+import org.jetbrains.skia.ColorType;
+import org.jetbrains.skia.impl.BufferUtil;
+import org.opencv.core.CvType;
+import org.opencv.core.Mat;
+import org.opencv.core.Size;
+import org.opencv.imgproc.Imgproc;
+
+import java.nio.ByteBuffer;
+
+public class Utils {
+
+ /**
+ * Converts Android Bitmap to OpenCV Mat.
+ *
+ * This function converts an Android Bitmap image to the OpenCV Mat.
+ *
'ARGB_8888' and 'RGB_565' input Bitmap formats are supported.
+ *
The output Mat is always created of the same size as the input Bitmap and of the 'CV_8UC4' type,
+ * it keeps the image in RGBA format.
+ *
This function throws an exception if the conversion fails.
+ * @param bmp is a valid input Bitmap object of the type 'ARGB_8888' or 'RGB_565'.
+ * @param mat is a valid output Mat object, it will be reallocated if needed, so it may be empty.
+ * @param unPremultiplyAlpha is a flag, that determines, whether the bitmap needs to be converted from alpha premultiplied format (like Android keeps 'ARGB_8888' ones) to regular one; this flag is ignored for 'RGB_565' bitmaps.
+ */
+ public static void bitmapToMat(Bitmap bmp, Mat mat, boolean unPremultiplyAlpha) {
+ if (bmp == null)
+ throw new IllegalArgumentException("bmp == null");
+ if (mat == null)
+ throw new IllegalArgumentException("mat == null");
+ nBitmapToMat2(bmp, mat, unPremultiplyAlpha);
+ }
+
+ /**
+ * Short form of the bitmapToMat(bmp, mat, unPremultiplyAlpha=false).
+ * @param bmp is a valid input Bitmap object of the type 'ARGB_8888' or 'RGB_565'.
+ * @param mat is a valid output Mat object, it will be reallocated if needed, so Mat may be empty.
+ */
+ public static void bitmapToMat(Bitmap bmp, Mat mat) {
+ bitmapToMat(bmp, mat, false);
+ }
+
+ /**
+ * Converts OpenCV Mat to Android Bitmap.
+ *
+ *
This function converts an image in the OpenCV Mat representation to the Android Bitmap.
+ *
The input Mat object has to be of the types 'CV_8UC1' (gray-scale), 'CV_8UC3' (RGB) or 'CV_8UC4' (RGBA).
+ *
The output Bitmap object has to be of the same size as the input Mat and of the types 'ARGB_8888' or 'RGB_565'.
+ *
This function throws an exception if the conversion fails.
+ *
+ * @param mat is a valid input Mat object of types 'CV_8UC1', 'CV_8UC3' or 'CV_8UC4'.
+ * @param bmp is a valid Bitmap object of the same size as the Mat and of type 'ARGB_8888' or 'RGB_565'.
+ * @param premultiplyAlpha is a flag, that determines, whether the Mat needs to be converted to alpha premultiplied format (like Android keeps 'ARGB_8888' bitmaps); the flag is ignored for 'RGB_565' bitmaps.
+ */
+ public static void matToBitmap(Mat mat, Bitmap bmp, boolean premultiplyAlpha) {
+ if (mat == null)
+ throw new IllegalArgumentException("mat == null");
+ if (bmp == null)
+ throw new IllegalArgumentException("bmp == null");
+ nMatToBitmap2(mat, bmp, premultiplyAlpha);
+ }
+
+ /**
+ * Short form of the matToBitmap(mat, bmp, premultiplyAlpha=false)
+ * @param mat is a valid input Mat object of the types 'CV_8UC1', 'CV_8UC3' or 'CV_8UC4'.
+ * @param bmp is a valid Bitmap object of the same size as the Mat and of type 'ARGB_8888' or 'RGB_565'.
+ */
+ public static void matToBitmap(Mat mat, Bitmap bmp) {
+ matToBitmap(mat, bmp, false);
+ }
+
+ private static void nBitmapToMat2(Bitmap b, Mat mat, boolean unPremultiplyAlpha) {
+ mat.create(new Size(b.getWidth(), b.getHeight()), CvType.CV_8UC4);
+
+ int size = b.getWidth() * b.getHeight() * 4;
+
+ long addr = b.theBitmap.peekPixels().getAddr();
+ ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, size);
+
+ if( b.theBitmap.getImageInfo().getColorType() == ColorType.RGBA_8888 )
+ {
+ Mat tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC4, buffer);
+ if(unPremultiplyAlpha) Imgproc.cvtColor(tmp, mat, Imgproc.COLOR_mRGBA2RGBA);
+ else tmp.copyTo(mat);
+
+ tmp.release();
+ } else {
+ // info.format == ANDROID_BITMAP_FORMAT_RGB_565
+ Mat tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC2, buffer);
+ Imgproc.cvtColor(tmp, mat, Imgproc.COLOR_BGR5652RGBA);
+
+ tmp.release();
+ }
+ }
+
+ private static byte[] m2bData = new byte[0];
+
+ private static void nMatToBitmap2(Mat src, Bitmap b, boolean premultiplyAlpha) {
+ Mat tmp;
+
+ if(b.getConfig() == Bitmap.Config.ARGB_8888) {
+ tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC4);
+
+ if(src.type() == CvType.CV_8UC1)
+ {
+ Imgproc.cvtColor(src, tmp, Imgproc.COLOR_GRAY2BGRA);
+ } else if(src.type() == CvType.CV_8UC3){
+ Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGB2BGRA);
+ } else if(src.type() == CvType.CV_8UC4){
+ if(premultiplyAlpha) Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2mRGBA);
+ else Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2BGRA);
+ }
+ } else {
+ tmp = new Mat(b.getWidth(), b.getHeight(), CvType.CV_8UC2);
+
+ if(src.type() == CvType.CV_8UC1)
+ {
+ Imgproc.cvtColor(src, tmp, Imgproc.COLOR_GRAY2BGR565);
+ } else if(src.type() == CvType.CV_8UC3){
+ Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGB2BGR565);
+ } else if(src.type() == CvType.CV_8UC4){
+ Imgproc.cvtColor(src, tmp, Imgproc.COLOR_RGBA2BGR565);
+ }
+ }
+
+ int size = tmp.rows() * tmp.cols() * tmp.channels();
+
+ if(m2bData.length != size) {
+ m2bData = new byte[size];
+ }
+
+ tmp.get(0, 0, m2bData);
+
+ long addr = b.theBitmap.peekPixels().getAddr();
+ ByteBuffer buffer = BufferUtil.INSTANCE.getByteBufferFromPointer(addr, size);
+
+ buffer.put(m2bData);
+
+ tmp.release();
+ }
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java
new file mode 100644
index 00000000..ca652252
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCamera.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+public interface OpenCvCamera
+{
+ public static final int CAMERA_OPEN_ERROR_FAILURE_TO_OPEN_CAMERA_DEVICE = -1;
+ public static final int CAMERA_OPEN_ERROR_POSTMORTEM_OPMODE = -2;
+
+ /***
+ * Open the connection to the camera device. If the camera is
+ * already open, this will not do anything.
+ *
+ * You must call this before calling:
+ * {@link #startStreaming(int, int)}
+ * or {@link #startStreaming(int, int, OpenCvCameraRotation)}
+ * or {@link #stopStreaming()}
+ *
+ * See {@link #closeCameraDevice()}
+ */
+ @Deprecated
+ int openCameraDevice();
+
+ /***
+ * Performs the same thing as {@link #openCameraDevice()} except
+ * in a non-blocking fashion, with a callback delivered to you
+ * when the operation is complete. This can be particularly helpful
+ * if using a webcam, as opening/starting streaming on a webcam can
+ * be very expensive time-wise.
+ *
+ * It is reccommended to start streaming from your listener:
+ *
+ * camera = OpenCvCameraFactory.create..............
+ * camera.setPipeline(new SomePipeline());
+ *
+ * camera.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener()
+ * {
+ * @Override
+ * public void onOpened()
+ * {
+ * camera.startStreaming(640, 480, OpenCvCameraRotation.UPRIGHT);
+ * }
+ * });
+ *
+ * NOTE: the operation performed in the background thread is synchronized
+ * with the main lock, so any calls to camera.XYZ() will block until the
+ * callback has been completed.
+ *
+ * @param cameraOpenListener the listener to which a callback will be
+ * delivered when the camera has been opened
+ */
+ void openCameraDeviceAsync(AsyncCameraOpenListener cameraOpenListener);
+
+ interface AsyncCameraOpenListener
+ {
+ /**
+ * Called if the camera was successfully opened
+ */
+ void onOpened();
+
+ /**
+ * Called if there was an error opening the camera
+ * @param errorCode reason for failure
+ */
+ void onError(int errorCode);
+ }
+
+ /***
+ * Close the connection to the camera device. If the camera is
+ * already closed, this will not do anything.
+ */
+ void closeCameraDevice();
+
+ /***
+ * Performs the same this as {@link #closeCameraDevice()} except
+ * in a non-blocking fashion.
+ *
+ * NOTE: the operation performed in the background thread is synchronized
+ * with the main lock, so any calls to camera.XYZ() will block until the
+ * callback has been completed.
+ *
+ * @param cameraCloseListener the listener to which a callback will be
+ * delivered when the camera has been closed
+ */
+ void closeCameraDeviceAsync(AsyncCameraCloseListener cameraCloseListener);
+
+ interface AsyncCameraCloseListener
+ {
+ void onClose();
+ }
+
+ /***
+ * If a viewport container ID was passed to the constructor of
+ * the implementing class, this method controls whether or not
+ * to show some info/statistics on top of the camera feed.
+ *
+ * @param show whether to show some info on top of the camera feed
+ */
+ void showFpsMeterOnViewport(boolean show);
+
+ /***
+ * If a viewport container ID was passed to the constructor of
+ * the implementing class, this method will "pause" the viewport
+ * rendering thread. This can reduce CPU, memory, and power load.
+ * For instance, this could be useful if you wish to see the live
+ * camera preview as you are initializing your robot, but you no
+ * longer require the live preview after you have finished your
+ * initialization process. See {@link #resumeViewport()}
+ */
+ void pauseViewport();
+
+ /***
+ * If a viewport container ID was passed to the constructor of
+ * the implementing class, and the viewport was previously paused
+ * by {@link #pauseViewport()}, this method will "unpause" the
+ * viewport rendering thread, so that you can see the live camera
+ * feed on the screen again.
+ */
+ void resumeViewport();
+
+ /***
+ * The way the viewport will render the live preview
+ *
+ * IMPORTANT NOTE: The policy you choose here has NO IMPACT on the
+ * frames passed to your pipeline. This ONLY affects how the frames
+ * you return from your pipeline are rendered to the viewport.
+ */
+ enum ViewportRenderingPolicy
+ {
+ /*
+ * This policy will minimize the CPU load caused by the viewport
+ * rendering, at the expense of displaying a preview which is 90
+ * or 180 out from what you might expect in some orientations.
+ * (Note: unlike when viewing a still picture which is taken sideways,
+ * simply rotating the phone physically does not correct the view
+ * because when doing so you also rotate the camera on the phone).
+ */
+ MAXIMIZE_EFFICIENCY,
+
+ /*
+ * This policy will ensure that the live view in the viewport is
+ * always displayed in a logical orientation, at the expense of
+ * additional CPU load.
+ */
+ OPTIMIZE_VIEW
+ }
+
+ /***
+ * Set the viewport rendering policy for this camera
+ *
+ * @param policy see {@link ViewportRenderingPolicy}
+ */
+ void setViewportRenderingPolicy(ViewportRenderingPolicy policy);
+
+ /***
+ * The renderer the viewport will use to render the live preview
+ * NOTE: this is different than {@link ViewportRenderingPolicy}.
+ * The rendering policy controls how the preview will look, but
+ * this controls how the rendering is *actually done*
+ */
+ enum ViewportRenderer
+ {
+ /**
+ * Default, if not otherwise specified. Historically this was the only option
+ * (Well, technically there wasn't an option for this at all before, but you get the idea)
+ */
+ SOFTWARE,
+
+ /**
+ * Can provide a much smoother live preview at higher resolutions, especially if
+ * you're using {@link ViewportRenderingPolicy#OPTIMIZE_VIEW}.
+ * However, using GPU acceleration has been observed to occasionally cause crashes
+ * in libgles.so / libutils.so on some devices, if the activity orientation is changed
+ * (i.e. you rotate the device) while a streaming session is in flight. Caveat emptor.
+ * Deprecated in favor of NATIVE_VIEW
+ */
+ @Deprecated
+ GPU_ACCELERATED,
+
+ /**
+ * Renders using the native Android view (which is hardware accelerated).
+ */
+ NATIVE_VIEW
+ }
+
+ /***
+ * Set the viewport renderer for this camera
+ * NOTE: This may ONLY be called if there is not currently a streaming session in
+ * flight for this camera.
+ *
+ * @param renderer see {@link ViewportRenderer}
+ * @throws IllegalStateException if called while a streaming session is in flight
+ */
+ void setViewportRenderer(ViewportRenderer renderer);
+
+ /***
+ * Tell the camera to start streaming images to us! Note that you must make sure
+ * the resolution you specify is supported by the camera. If it is not, an exception
+ * will be thrown.
+ *
+ * Keep in mind that the SDK's UVC driver (what OpenCvWebcam uses under the hood) only
+ * supports streaming from the webcam in the uncompressed YUV image format. This means
+ * that the maximum resolution you can stream at and still get up to 30FPS is 480p (640x480).
+ * Streaming at e.g. 720p will limit you to up to 10FPS and so on and so forth.
+ *
+ * Also see the alternate {@link #startStreaming(int, int, OpenCvCameraRotation)} method.
+ *
+ * @param width the width of the resolution in which you would like the camera to stream
+ * @param height the height of the resolution in which you would like the camera to stream
+ */
+ void startStreaming(int width, int height);
+
+ /***
+ * Same as {@link #startStreaming(int, int)} except for:
+ *
+ * @param rotation the rotation that the camera is being used in. This is so that
+ * the image from the camera sensor can be rotated such that it is always
+ * displayed with the image upright. For a front facing camera, rotation is
+ * defined assuming the user is looking at the screen. For a rear facing camera
+ * or a webcam, rotation is defined assuming the camera is facing away from the user.
+ */
+ void startStreaming(int width, int height, OpenCvCameraRotation rotation);
+
+ /***
+ * Stops streaming images from the camera (and, by extension, stops invoking your vision
+ * pipeline), without closing ({@link #closeCameraDevice()}) the connection to the camera.
+ */
+ void stopStreaming();
+
+ /***
+ * Specify the image processing pipeline that you wish to be invoked upon receipt
+ * of each frame from the camera. Note that switching pipelines on-the-fly (while
+ * a streaming session is in flight) *IS* supported.
+ *
+ * @param pipeline the image processing pipeline that you wish to be invoked upon
+ * receipt of each frame from the camera.
+ */
+ void setPipeline(OpenCvPipeline pipeline);
+
+ /***
+ * Get the number of frames that have been received from the camera and processed by
+ * your pipeline since {@link #startStreaming(int, int)} was called.
+ *
+ * @return the number of frames that have been received from the camera and processed
+ * by your pipeline since {@link #startStreaming(int, int)} was called.
+ */
+ int getFrameCount();
+
+ /***
+ * Get the current frame rate of the overall system (including your pipeline as well as
+ * overhead) averaged over the last 30 frames.
+ *
+ * @return the current frame rate of the overall system (including your pipeline as well
+ * as overhead) averaged over the last 30 frames.
+ */
+ float getFps();
+
+ /***
+ * Get the current execution time (in milliseconds) of your pipeline, averaged over the
+ * last 30 frames.
+ *
+ * @return the current execution time (in milliseconds) of your pipeline, averaged
+ * over the last 30 frames.
+ */
+ int getPipelineTimeMs();
+
+ /***
+ * Get the current system overhead time (in milliseconds) for each frame, averaged over
+ * the last 30 frames.
+ *
+ * @return the current system overhead time (in milliseconds) for each frame, averaged
+ * over the last 30 frames
+ */
+ int getOverheadTimeMs();
+
+ /***
+ * Get the current total processing time (in milliseconds) for each frame (including
+ * pipeline and overhead), averaged over the last 30 frames.
+ *
+ * @return the current total processing time (in milliseconds) for each frame (including
+ * pipeline and overhead), averaged over the last 30 frames.
+ */
+ int getTotalFrameTimeMs();
+
+ /***
+ * Get the current theoretically maximum frame rate that your pipeline (and overhead)
+ * could achieve. This is useful for identifying whether or not your pipeline is the
+ * bottleneck in the system. For instance, if {@link #getFps()} reports that the system
+ * is running at 10FPS, and this method reported that your theoretical maximum FPS is
+ * 12, then your pipeline is the bottleneck. Conversely, if {@link #getFps()} reported that
+ * the system was running at 25FPS, and this method reported that your theoretical maximum
+ * FPS is 100, then the camera would be the bottleneck.
+ *
+ * @return the current theoretically maximum frame rate that your pipeline (and overhead)
+ * could achieve.
+ */
+ int getCurrentPipelineMaxFps();
+
+ /***
+ * Start recording the output of the camera's current pipeline
+ * (If no pipeline is set, then the plain camera image is recorded)
+ * A streaming session must be in flight before this can be called.
+ * The recording will be automatically stopped when the streaming
+ * session is stopped (whether that be manually or automatically at
+ * the end of the OpMode), but can also be stopped independently by
+ * calling {@link #stopRecordingPipeline()}
+ *
+ * @param parameters the parameters which define how the recording should done
+ * @throws IllegalStateException if called before streaming is started
+ * @throws IllegalStateException if recording was started previously
+ */
+ void startRecordingPipeline(PipelineRecordingParameters parameters);
+
+ /***
+ * Stops recording the output of the camera's current pipeline,
+ * if a recording session is currently active.
+ */
+ void stopRecordingPipeline();
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java
new file mode 100644
index 00000000..8c0ce412
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraBase.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import com.qualcomm.robotcore.util.ElapsedTime;
+import com.qualcomm.robotcore.util.MovingStatistics;
+import io.github.deltacv.common.pipeline.util.PipelineStatisticsCalculator;
+import io.github.deltacv.vision.external.PipelineRenderHook;
+import org.opencv.android.Utils;
+import org.opencv.core.*;
+import org.opencv.imgproc.Imgproc;
+
+public abstract class OpenCvCameraBase implements OpenCvCamera {
+
+ private OpenCvPipeline pipeline = null;
+
+ private OpenCvViewport viewport;
+ private OpenCvCameraRotation rotation;
+
+ private final Object pipelineChangeLock = new Object();
+
+ private Mat rotatedMat = new Mat();
+ private Mat matToUseIfPipelineReturnedCropped;
+ private Mat croppedColorCvtedMat = new Mat();
+
+ private boolean isStreaming = false;
+ private boolean viewportEnabled = true;
+
+ private ViewportRenderer desiredViewportRenderer = ViewportRenderer.SOFTWARE;
+ ViewportRenderingPolicy desiredRenderingPolicy = ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY;
+ boolean fpsMeterDesired = true;
+
+ private Scalar brown = new Scalar(82, 61, 46, 255);
+
+ private int frameCount = 0;
+
+ private PipelineStatisticsCalculator statistics = new PipelineStatisticsCalculator();
+
+ private double width;
+ private double height;
+
+ public OpenCvCameraBase(OpenCvViewport viewport, boolean viewportEnabled) {
+ this.viewport = viewport;
+ this.rotation = getDefaultRotation();
+
+ this.viewportEnabled = viewportEnabled;
+ }
+
+ @Override
+ public void showFpsMeterOnViewport(boolean show) {
+ viewport.setFpsMeterEnabled(show);
+ }
+
+ @Override
+ public void pauseViewport() {
+ viewport.pause();
+ }
+
+ @Override
+ public void resumeViewport() {
+ viewport.resume();
+ }
+
+ @Override
+ public void setViewportRenderingPolicy(ViewportRenderingPolicy policy) {
+ viewport.setRenderingPolicy(policy);
+ }
+
+ @Override
+ public void setViewportRenderer(ViewportRenderer renderer) {
+ this.desiredViewportRenderer = renderer;
+ }
+
+ @Override
+ public void setPipeline(OpenCvPipeline pipeline) {
+ this.pipeline = pipeline;
+ }
+
+ @Override
+ public int getFrameCount() {
+ return 0;
+ }
+
+ @Override
+ public float getFps() {
+ return statistics.getAvgFps();
+ }
+
+ @Override
+ public int getPipelineTimeMs() {
+ return statistics.getAvgPipelineTime();
+ }
+
+ @Override
+ public int getOverheadTimeMs() {
+ return statistics.getAvgOverheadTime();
+ }
+
+ @Override
+ public int getTotalFrameTimeMs() {
+ return getTotalFrameTimeMs();
+ }
+
+ @Override
+ public int getCurrentPipelineMaxFps() {
+ return 0;
+ }
+
+ @Override
+ public void startRecordingPipeline(PipelineRecordingParameters parameters) {
+ }
+
+ @Override
+ public void stopRecordingPipeline() {
+ }
+
+ protected void notifyStartOfFrameProcessing() {
+ statistics.newInputFrameStart();
+ }
+
+ public synchronized final void prepareForOpenCameraDevice()
+ {
+ if (viewportEnabled)
+ {
+ setupViewport();
+ viewport.setRenderingPolicy(desiredRenderingPolicy);
+ }
+ }
+
+ public synchronized final void prepareForStartStreaming(int width, int height, OpenCvCameraRotation rotation)
+ {
+ this.rotation = rotation;
+ this.statistics = new PipelineStatisticsCalculator();
+ this.statistics.init();
+
+ Size sizeAfterRotation = getFrameSizeAfterRotation(width, height, rotation);
+
+ this.width = sizeAfterRotation.width;
+ this.height = sizeAfterRotation.height;
+
+ if(viewport != null)
+ {
+ // viewport.setSize(width, height);
+ viewport.setOptimizedViewRotation(getOptimizedViewportRotation(rotation));
+ viewport.activate();
+ }
+ }
+
+ public synchronized final void cleanupForEndStreaming() {
+ matToUseIfPipelineReturnedCropped = null;
+
+ if (viewport != null) {
+ viewport.deactivate();
+ }
+ }
+
+ protected synchronized void handleFrameUserCrashable(Mat frame, long timestamp) {
+ statistics.newPipelineFrameStart();
+
+ Mat userProcessedFrame = null;
+
+ int rotateCode = mapRotationEnumToOpenCvRotateCode(rotation);
+
+ if (rotateCode != -1) {
+ /*
+ * Rotate onto another Mat rather than doing so in-place.
+ *
+ * This does two things:
+ * 1) It seems that rotating by 90 or 270 in-place
+ * causes the backing buffer to be re-allocated
+ * since the width/height becomes swapped. This
+ * causes a problem for user code which makes a
+ * submat from the input Mat, because after the
+ * parent Mat is re-allocated the submat is no
+ * longer tied to it. Thus, by rotating onto
+ * another Mat (which is never re-allocated) we
+ * remove that issue.
+ *
+ * 2) Since the backing buffer does need need to be
+ * re-allocated for each frame, we reduce overhead
+ * time by about 1ms.
+ */
+ Core.rotate(frame, rotatedMat, rotateCode);
+ frame = rotatedMat;
+ }
+
+ final OpenCvPipeline pipelineSafe;
+
+ // Grab a safe reference to what the pipeline currently is,
+ // since the user is allowed to change it at any time
+ synchronized (pipelineChangeLock) {
+ pipelineSafe = pipeline;
+ }
+
+ if (pipelineSafe != null) {
+ if (pipelineSafe instanceof TimestampedOpenCvPipeline) {
+ ((TimestampedOpenCvPipeline) pipelineSafe).setTimestamp(timestamp);
+ }
+
+ statistics.beforeProcessFrame();
+
+ userProcessedFrame = pipelineSafe.processFrameInternal(frame);
+
+ statistics.afterProcessFrame();
+ }
+
+ // Will point to whatever mat we end up deciding to send to the screen
+ final Mat matForDisplay;
+
+ if (pipelineSafe == null) {
+ matForDisplay = frame;
+ } else if (userProcessedFrame == null) {
+ throw new OpenCvCameraException("User pipeline returned null");
+ } else if (userProcessedFrame.empty()) {
+ throw new OpenCvCameraException("User pipeline returned empty mat");
+ } else if (userProcessedFrame.cols() != frame.cols() || userProcessedFrame.rows() != frame.rows()) {
+ /*
+ * The user didn't return the same size image from their pipeline as we gave them,
+ * ugh. This makes our lives interesting because we can't just send an arbitrary
+ * frame size to the viewport. It re-uses framebuffers that are of a fixed resolution.
+ * So, we copy the user's Mat onto a Mat of the correct size, and then send that other
+ * Mat to the viewport.
+ */
+
+ if (userProcessedFrame.cols() > frame.cols() || userProcessedFrame.rows() > frame.rows()) {
+ /*
+ * What on earth was this user thinking?! They returned a Mat that's BIGGER in
+ * a dimension than the one we gave them!
+ */
+
+ throw new OpenCvCameraException("User pipeline returned frame of unexpected size");
+ }
+
+ //We re-use this buffer, only create if needed
+ if (matToUseIfPipelineReturnedCropped == null) {
+ matToUseIfPipelineReturnedCropped = frame.clone();
+ }
+
+ //Set to brown to indicate to the user the areas which they cropped off
+ matToUseIfPipelineReturnedCropped.setTo(brown);
+
+ int usrFrmTyp = userProcessedFrame.type();
+
+ if (usrFrmTyp == CvType.CV_8UC1) {
+ /*
+ * Handle 8UC1 returns (masks and single channels of images);
+ *
+ * We have to color convert onto a different mat (rather than
+ * doing so in place) to avoid breaking any of the user's submats
+ */
+ Imgproc.cvtColor(userProcessedFrame, croppedColorCvtedMat, Imgproc.COLOR_GRAY2RGBA);
+ userProcessedFrame = croppedColorCvtedMat; //Doesn't affect user's handle, only ours
+ } else if (usrFrmTyp != CvType.CV_8UC4 && usrFrmTyp != CvType.CV_8UC3) {
+ /*
+ * Oof, we don't know how to handle the type they gave us
+ */
+ throw new OpenCvCameraException("User pipeline returned a frame of an illegal type. Valid types are CV_8UC1, CV_8UC3, and CV_8UC4");
+ }
+
+ //Copy the user's frame onto a Mat of the correct size
+ userProcessedFrame.copyTo(matToUseIfPipelineReturnedCropped.submat(
+ new Rect(0, 0, userProcessedFrame.cols(), userProcessedFrame.rows())));
+
+ //Send that correct size Mat to the viewport
+ matForDisplay = matToUseIfPipelineReturnedCropped;
+ } else {
+ /*
+ * Yay, smart user! They gave us the frame size we were expecting!
+ * Go ahead and send it right on over to the viewport.
+ */
+ matForDisplay = userProcessedFrame;
+ }
+
+ if (viewport != null) {
+ viewport.post(matForDisplay, new OpenCvViewport.FrameContext(pipelineSafe, pipelineSafe != null ? pipelineSafe.getUserContextForDrawHook() : null));
+ }
+
+ statistics.endFrame();
+
+ if (viewport != null) {
+ viewport.notifyStatistics(statistics.getAvgFps(), statistics.getAvgPipelineTime(), statistics.getAvgOverheadTime());
+ }
+
+ frameCount++;
+ }
+
+
+ protected OpenCvViewport.OptimizedRotation getOptimizedViewportRotation(OpenCvCameraRotation streamRotation) {
+ if (!cameraOrientationIsTiedToDeviceOrientation()) {
+ return OpenCvViewport.OptimizedRotation.NONE;
+ }
+
+ if (streamRotation == OpenCvCameraRotation.SIDEWAYS_LEFT || streamRotation == OpenCvCameraRotation.SENSOR_NATIVE) {
+ return OpenCvViewport.OptimizedRotation.ROT_90_COUNTERCLOCWISE;
+ } else if (streamRotation == OpenCvCameraRotation.SIDEWAYS_RIGHT) {
+ return OpenCvViewport.OptimizedRotation.ROT_90_CLOCKWISE;
+ } else if (streamRotation == OpenCvCameraRotation.UPSIDE_DOWN) {
+ return OpenCvViewport.OptimizedRotation.ROT_180;
+ } else {
+ return OpenCvViewport.OptimizedRotation.NONE;
+ }
+ }
+
+ protected void setupViewport() {
+ viewport.setFpsMeterEnabled(fpsMeterDesired);
+ viewport.setRenderHook(PipelineRenderHook.INSTANCE);
+ }
+
+ protected abstract OpenCvCameraRotation getDefaultRotation();
+
+ protected abstract int mapRotationEnumToOpenCvRotateCode(OpenCvCameraRotation rotation);
+
+ protected abstract boolean cameraOrientationIsTiedToDeviceOrientation();
+
+ protected abstract boolean isStreaming();
+
+ protected Size getFrameSizeAfterRotation(int width, int height, OpenCvCameraRotation rotation)
+ {
+ int screenRenderedWidth, screenRenderedHeight;
+ int openCvRotateCode = mapRotationEnumToOpenCvRotateCode(rotation);
+
+ if(openCvRotateCode == Core.ROTATE_90_CLOCKWISE || openCvRotateCode == Core.ROTATE_90_COUNTERCLOCKWISE)
+ {
+ //noinspection SuspiciousNameCombination
+ screenRenderedWidth = height;
+ //noinspection SuspiciousNameCombination
+ screenRenderedHeight = width;
+ }
+ else
+ {
+ screenRenderedWidth = width;
+ screenRenderedHeight = height;
+ }
+
+ return new Size(screenRenderedWidth, screenRenderedHeight);
+ }
+
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java
new file mode 100644
index 00000000..bd85888c
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+public class OpenCvCameraException extends RuntimeException
+{
+ public OpenCvCameraException(String msg)
+ {
+ super(msg);
+ }
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java
new file mode 100644
index 00000000..f417bd0c
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraFactory.java
@@ -0,0 +1,38 @@
+package org.openftc.easyopencv;
+
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+
+public abstract class OpenCvCameraFactory {
+
+ private static OpenCvCameraFactory instance = new SourcedOpenCvCameraFactoryImpl();
+
+ public static OpenCvCameraFactory getInstance() {
+ return instance;
+ }
+
+
+ /*
+ * Internal
+ */
+ public abstract OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction);
+ public abstract OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction, int viewportContainerId);
+
+ /*
+ * Internal2
+ */
+ public abstract OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction);
+ public abstract OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction, int viewportContainerId);
+
+ /*
+ * Webcam
+ */
+ public abstract OpenCvWebcam createWebcam(WebcamName cameraName);
+ public abstract OpenCvWebcam createWebcam(WebcamName cameraName, int viewportContainerId);
+
+ public enum ViewportSplitMethod
+ {
+ VERTICALLY,
+ HORIZONTALLY
+ }
+
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java
new file mode 100644
index 00000000..d8db2572
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvCameraRotation.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+public enum OpenCvCameraRotation
+{
+ UPRIGHT,
+ UPSIDE_DOWN,
+ SIDEWAYS_LEFT,
+ SIDEWAYS_RIGHT,
+ SENSOR_NATIVE,
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java
new file mode 100644
index 00000000..17f698fe
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import android.hardware.Camera;
+
+public interface OpenCvInternalCamera extends OpenCvCamera
+{
+ enum CameraDirection
+ {
+ FRONT(Camera.CameraInfo.CAMERA_FACING_FRONT),
+ BACK(Camera.CameraInfo.CAMERA_FACING_BACK);
+
+ public int id;
+
+ CameraDirection(int id)
+ {
+ this.id = id;
+ }
+ }
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java
new file mode 100644
index 00000000..0650622a
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvInternalCamera2.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import android.hardware.Camera;
+
+public interface OpenCvInternalCamera2 extends OpenCvCamera
+{
+ enum CameraDirection
+ {
+ FRONT(Camera.CameraInfo.CAMERA_FACING_FRONT),
+ BACK(Camera.CameraInfo.CAMERA_FACING_BACK);
+
+ public int id;
+
+ CameraDirection(int id)
+ {
+ this.id = id;
+ }
+ }
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java
new file mode 100644
index 00000000..13cac400
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java
@@ -0,0 +1,73 @@
+package org.openftc.easyopencv;
+
+import android.graphics.Canvas;
+import org.opencv.core.Mat;
+
+public abstract class OpenCvPipeline {
+
+ private Object userContext;
+ private boolean isFirstFrame = true;
+ private long firstFrameTimestamp;
+
+ public abstract Mat processFrame(Mat input);
+
+ public void onViewportTapped() { }
+
+ public void init(Mat mat) { }
+ public Object getUserContextForDrawHook()
+ {
+ return userContext;
+ }
+
+ /**
+ * Call this during processFrame() to request a hook during the viewport's
+ * drawing operation of the current frame (which will happen asynchronously
+ * at some future time) using the Canvas API.
+ *
+ * If you call this more than once during processFrame(), the last call takes
+ * precedence. You will only get a single draw hook for a given frame.
+ *
+ * @param userContext anything you want :monkey: will be passed back to you
+ * in {@link #onDrawFrame(Canvas, int, int, float, float, Object)}. You can
+ * use this to store information about what you found in the frame, so that
+ * you know what to draw when it's time. (Otherwise how the heck would you
+ * know what to draw??).
+ */
+ public void requestViewportDrawHook(Object userContext)
+ {
+ this.userContext = userContext;
+ }
+
+ /**
+ * Called during the viewport's frame rendering operation at some later point after
+ * you called called {@link #requestViewportDrawHook(Object)} during processFrame().
+ * Allows you to use the Canvas API to draw annotations on the frame, rather than
+ * using OpenCV calls. This allows for more eye-candy-y annotations since you've got
+ * a high resolution canvas to work with rather than, say, a 320x240 image.
+ *
+ * Note that this is NOT called from the same thread that calls processFrame()!
+ * And may actually be called from the UI thread depending on the viewport renderer.
+ *
+ * @param canvas the canvas that's being drawn on NOTE: Do NOT get dimensions from it, use below
+ * @param onscreenWidth the width of the canvas that corresponds to the image
+ * @param onscreenHeight the height of the canvas that corresponds to the image
+ * @param scaleBmpPxToCanvasPx multiply pixel coords by this to scale to canvas coords
+ * @param scaleCanvasDensity a scaling factor to adjust e.g. text size. Relative to Nexus5 DPI.
+ * @param userContext whatever you passed in when requesting the draw hook :monkey:
+ */
+ public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float scaleCanvasDensity, Object userContext) {};
+
+ Mat processFrameInternal(Mat input)
+ {
+ if(isFirstFrame)
+ {
+ init(input);
+
+ firstFrameTimestamp = System.currentTimeMillis();
+ isFirstFrame = false;
+ }
+
+ return processFrame(input);
+ }
+
+}
\ No newline at end of file
diff --git a/Common/src/main/java/org/openftc/easyopencv/OpenCvTracker.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTracker.java
similarity index 97%
rename from Common/src/main/java/org/openftc/easyopencv/OpenCvTracker.java
rename to Vision/src/main/java/org/openftc/easyopencv/OpenCvTracker.java
index a1422e4f..a0042a68 100644
--- a/Common/src/main/java/org/openftc/easyopencv/OpenCvTracker.java
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTracker.java
@@ -1,35 +1,35 @@
-/*
- * Copyright (c) 2019 OpenFTC Team
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.openftc.easyopencv;
-
-import org.opencv.core.Mat;
-
-public abstract class OpenCvTracker {
- private final Mat mat = new Mat();
-
- public abstract Mat processFrame(Mat input);
-
- protected final Mat processFrameInternal(Mat input) {
- input.copyTo(mat);
- return processFrame(mat);
- }
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import org.opencv.core.Mat;
+
+public abstract class OpenCvTracker {
+ private final Mat mat = new Mat();
+
+ public abstract Mat processFrame(Mat input);
+
+ protected final Mat processFrameInternal(Mat input) {
+ input.copyTo(mat);
+ return processFrame(mat);
+ }
}
\ No newline at end of file
diff --git a/Common/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java
similarity index 97%
rename from Common/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java
rename to Vision/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java
index 1f4c4504..fb3bb12b 100644
--- a/Common/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java
@@ -1,71 +1,73 @@
-/*
- * Copyright (c) 2019 OpenFTC Team
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.openftc.easyopencv;
-
-import org.opencv.core.Mat;
-
-import java.util.ArrayList;
-
-public class OpenCvTrackerApiPipeline extends OpenCvPipeline {
- private final ArrayList trackers = new ArrayList<>();
- private int trackerDisplayIdx = 0;
-
- public synchronized void addTracker(OpenCvTracker tracker) {
- trackers.add(tracker);
- }
-
- public synchronized void removeTracker(OpenCvTracker tracker) {
- trackers.remove(tracker);
-
- if (trackerDisplayIdx >= trackers.size()) {
- trackerDisplayIdx--;
-
- if (trackerDisplayIdx < 0) {
- trackerDisplayIdx = 0;
- }
- }
- }
-
- @Override
- public synchronized Mat processFrame(Mat input) {
- if (trackers.size() == 0) {
- return input;
- }
-
- ArrayList returnMats = new ArrayList<>(trackers.size());
-
- for (OpenCvTracker tracker : trackers) {
- returnMats.add(tracker.processFrameInternal(input));
- }
-
- return returnMats.get(trackerDisplayIdx);
- }
-
- @Override
- public synchronized void onViewportTapped() {
- trackerDisplayIdx++;
-
- if (trackerDisplayIdx >= trackers.size()) {
- trackerDisplayIdx = 0;
- }
- }
+/*
+ * Copyright (c) 2019 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import com.qualcomm.robotcore.eventloop.opmode.Disabled;
+import org.opencv.core.Mat;
+
+import java.util.ArrayList;
+
+@Disabled
+public class OpenCvTrackerApiPipeline extends OpenCvPipeline {
+ private final ArrayList trackers = new ArrayList<>();
+ private int trackerDisplayIdx = 0;
+
+ public synchronized void addTracker(OpenCvTracker tracker) {
+ trackers.add(tracker);
+ }
+
+ public synchronized void removeTracker(OpenCvTracker tracker) {
+ trackers.remove(tracker);
+
+ if (trackerDisplayIdx >= trackers.size()) {
+ trackerDisplayIdx--;
+
+ if (trackerDisplayIdx < 0) {
+ trackerDisplayIdx = 0;
+ }
+ }
+ }
+
+ @Override
+ public synchronized Mat processFrame(Mat input) {
+ if (trackers.size() == 0) {
+ return input;
+ }
+
+ ArrayList returnMats = new ArrayList<>(trackers.size());
+
+ for (OpenCvTracker tracker : trackers) {
+ returnMats.add(tracker.processFrameInternal(input));
+ }
+
+ return returnMats.get(trackerDisplayIdx);
+ }
+
+ @Override
+ public synchronized void onViewportTapped() {
+ trackerDisplayIdx++;
+
+ if (trackerDisplayIdx >= trackers.size()) {
+ trackerDisplayIdx = 0;
+ }
+ }
}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java
new file mode 100644
index 00000000..39596a3c
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewRenderer.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (c) 2023 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import org.opencv.android.Utils;
+import org.opencv.core.Mat;
+
+public class OpenCvViewRenderer
+{
+ private final int statBoxW;
+ private final int statBoxH;
+ private final int statBoxTextLineSpacing;
+ private final int statBoxTextFirstLineYFromBottomOffset;
+ private final int statBoxLTxtMargin;
+ private static final float referenceDPI = 443; // Nexus 5
+ private final float metricsScale;
+ private static final int OVERLAY_COLOR = Color.rgb(102, 20, 68);
+ private static final int PAUSED_COLOR = Color.rgb(255, 166, 0);
+ private static final int RC_ACTIVITY_BG_COLOR = Color.rgb(239,239,239);
+ private Paint fpsMeterNormalBgPaint;
+ private Paint fpsMeterRecordingPaint;
+ private Paint fpsMeterTextPaint;
+ private final float fpsMeterTextSize;
+ private Paint paintBlackBackground;
+ private double aspectRatio;
+
+ private boolean fpsMeterEnabled = true;
+ private String fpsMeterDescriptor;
+ private volatile float fps = 0;
+ private volatile int pipelineMs = 0;
+ private volatile int overheadMs = 0;
+
+ private int width;
+ private int height;
+ private final boolean offscreen;
+
+ private volatile boolean isRecording;
+
+ private volatile OpenCvViewport.OptimizedRotation optimizedViewRotation;
+
+ private volatile OpenCvCamera.ViewportRenderingPolicy renderingPolicy = OpenCvCamera.ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY;
+
+ private Bitmap bitmapFromMat;
+
+ public OpenCvViewRenderer(boolean renderingOffsceen, String fpsMeterDescriptor)
+ {
+ offscreen = renderingOffsceen;
+ this.fpsMeterDescriptor = fpsMeterDescriptor;
+
+ metricsScale = 1.0f;
+
+ fpsMeterTextSize = 26.2f * metricsScale;
+ statBoxW = (int) (450 * metricsScale);
+ statBoxH = (int) (120 * metricsScale);
+ statBoxTextLineSpacing = (int) (35 * metricsScale);
+ statBoxLTxtMargin = (int) (5 * metricsScale);
+ statBoxTextFirstLineYFromBottomOffset = (int) (80*metricsScale);
+
+ fpsMeterNormalBgPaint = new Paint();
+ fpsMeterNormalBgPaint.setColor(OVERLAY_COLOR);
+ fpsMeterNormalBgPaint.setStyle(Paint.Style.FILL);
+
+ fpsMeterRecordingPaint = new Paint();
+ fpsMeterRecordingPaint.setColor(Color.RED);
+ fpsMeterRecordingPaint.setStyle(Paint.Style.FILL);
+
+ fpsMeterTextPaint = new Paint();
+ fpsMeterTextPaint.setColor(Color.WHITE);
+ fpsMeterTextPaint.setTextSize(fpsMeterTextSize);
+ fpsMeterTextPaint.setAntiAlias(true);
+
+ paintBlackBackground = new Paint();
+ paintBlackBackground.setColor(Color.BLACK);
+ paintBlackBackground.setStyle(Paint.Style.FILL);
+ }
+
+ private void unifiedDraw(Canvas canvas, int onscreenWidth, int onscreenHeight, OpenCvViewport.RenderHook userHook, Object userCtx)
+ {
+ int x_offset_statbox = 0;
+ int y_offset_statbox = 0;
+
+ int topLeftX;
+ int topLeftY;
+ int scaledWidth;
+ int scaledHeight;
+
+ double canvasAspect = (float) onscreenWidth/onscreenHeight;
+
+ if(aspectRatio > canvasAspect) /* Image is WIDER than canvas */
+ {
+ // Width: we use the max we have, since horizontal bounds are hit before vertical bounds
+ scaledWidth = onscreenWidth;
+
+ // Height: calculate a scaled height assuming width is maxed for the canvas
+ scaledHeight = (int) Math.round(onscreenWidth / aspectRatio);
+
+ // We want to center the image in the viewport
+ topLeftY = Math.abs(onscreenHeight-scaledHeight)/2;
+ topLeftX = 0;
+ y_offset_statbox = topLeftY;
+ }
+ else /* Image is TALLER than canvas */
+ {
+ // Height: we use the max we have, since vertical bounds are hit before horizontal bounds
+ scaledHeight = onscreenHeight;
+
+ // Width: calculate a scaled width assuming height is maxed for the canvas
+ scaledWidth = (int) Math.round(onscreenHeight * aspectRatio);
+
+ // We want to center the image in the viewport
+ topLeftY = 0;
+ topLeftX = Math.abs(onscreenWidth - scaledWidth) / 2;
+ x_offset_statbox = topLeftX;
+ }
+
+ //Draw the bitmap, scaling it to the maximum size that will fit in the viewport
+ Rect bmpRect = createRect(
+ topLeftX,
+ topLeftY,
+ scaledWidth,
+ scaledHeight);
+
+ // Draw black behind the bitmap to avoid alpha issues if usercode tries to draw
+ // annotations and doesn't specify alpha 255. This wasn't an issue when we just
+ // painted black behind the entire view, but now that we paint the RC background
+ // color, it is an issue...
+ canvas.drawRect(bmpRect, paintBlackBackground);
+
+ canvas.drawBitmap(
+ bitmapFromMat,
+ null,
+ bmpRect,
+ null
+ );
+
+ // We need to save the canvas translation/rotation and such before we hand off to the user,
+ // because if they don't put it back how they found it and then we go to draw the FPS meter,
+ // it's... well... not going to draw properly lol
+ int canvasSaveBeforeUserDraw = canvas.save();
+
+ // Allow the user to do some drawing if they want
+ if (userHook != null)
+ {
+ // Can either use width or height I guess ¯\_(ツ)_/¯
+ float scaleBitmapPxToCanvasPx = (float) scaledWidth / bitmapFromMat.getWidth();
+
+ // To make the user's life easy, we teleport the origin to the top
+ // left corner of the bitmap we painted
+ canvas.translate(topLeftX, topLeftY);
+ userHook.onDrawFrame(canvas, scaledWidth, scaledHeight, scaleBitmapPxToCanvasPx, metricsScale, userCtx);
+ }
+
+ // Make sure the canvas translation/rotation is what we expect (see comment when we save state)
+ canvas.restoreToCount(canvasSaveBeforeUserDraw);
+
+ if (fpsMeterEnabled)
+ {
+ Rect statsRect = createRect(
+ x_offset_statbox,
+ onscreenHeight-y_offset_statbox-statBoxH,
+ statBoxW,
+ statBoxH
+ );
+
+ drawStats(canvas, statsRect);
+ }
+ }
+
+ private void drawStats(Canvas canvas, Rect rect)
+ {
+ // Draw the purple rectangle
+ if(isRecording)
+ {
+ canvas.drawRect(rect, fpsMeterRecordingPaint);
+ }
+ else
+ {
+ canvas.drawRect(rect, fpsMeterNormalBgPaint);
+ }
+
+ // Some formatting stuff
+ int statBoxLTxtStart = rect.left+statBoxLTxtMargin;
+ int textLine1Y = rect.bottom - statBoxTextFirstLineYFromBottomOffset;
+ int textLine2Y = textLine1Y + statBoxTextLineSpacing;
+ int textLine3Y = textLine2Y + statBoxTextLineSpacing;
+
+ // Draw the 3 text lines
+ canvas.drawText(fpsMeterDescriptor, statBoxLTxtStart, textLine1Y, fpsMeterTextPaint);
+ canvas.drawText(String.format("FPS@%dx%d: %.2f", width, height, fps), statBoxLTxtStart, textLine2Y, fpsMeterTextPaint);
+ canvas.drawText(String.format("Pipeline: %dms - Overhead: %dms", pipelineMs, overheadMs), statBoxLTxtStart, textLine3Y, fpsMeterTextPaint);
+ }
+
+ Rect createRect(int tlx, int tly, int w, int h)
+ {
+ return new Rect(tlx, tly, tlx+w, tly+h);
+ }
+
+ public void setFpsMeterEnabled(boolean fpsMeterEnabled)
+ {
+ this.fpsMeterEnabled = fpsMeterEnabled;
+ }
+
+ public void notifyStatistics(float fps, int pipelineMs, int overheadMs)
+ {
+ this.fps = fps;
+ this.pipelineMs = pipelineMs;
+ this.overheadMs = overheadMs;
+ }
+
+ public void setRecording(boolean recording)
+ {
+ isRecording = recording;
+ }
+
+ public void setOptimizedViewRotation(OpenCvViewport.OptimizedRotation optimizedViewRotation)
+ {
+ this.optimizedViewRotation = optimizedViewRotation;
+ }
+
+ public void render(Mat mat, Canvas canvas, OpenCvViewport.RenderHook userHook, Object userCtx)
+ {
+ if (bitmapFromMat == null || bitmapFromMat.getWidth() != mat.width() || bitmapFromMat.getHeight() != mat.height())
+ {
+ if (bitmapFromMat != null)
+ {
+ bitmapFromMat.recycle();
+ }
+
+ bitmapFromMat = Bitmap.createBitmap(mat.width(), mat.height(), Bitmap.Config.ARGB_8888);
+ }
+
+ //Convert that Mat to a bitmap we can render
+ Utils.matToBitmap(mat, bitmapFromMat, false);
+
+ width = bitmapFromMat.getWidth();
+ height = bitmapFromMat.getHeight();
+ aspectRatio = (float) width / height;
+
+ // Cache current state, can change behind our backs
+ OpenCvViewport.OptimizedRotation optimizedRotationSafe = optimizedViewRotation;
+
+ if(renderingPolicy == OpenCvCamera.ViewportRenderingPolicy.MAXIMIZE_EFFICIENCY || optimizedRotationSafe == OpenCvViewport.OptimizedRotation.NONE)
+ {
+ unifiedDraw(canvas, canvas.getWidth(), canvas.getHeight(), userHook, userCtx);
+ }
+ else if(renderingPolicy == OpenCvCamera.ViewportRenderingPolicy.OPTIMIZE_VIEW)
+ {
+ if(optimizedRotationSafe == OpenCvViewport.OptimizedRotation.ROT_180)
+ {
+ // 180 is easy, just rotate canvas 180 about center and draw as usual
+ canvas.rotate(optimizedRotationSafe.val, canvas.getWidth()/2, canvas.getHeight()/2);
+ unifiedDraw(canvas, canvas.getWidth(), canvas.getHeight(), userHook, userCtx);
+ }
+ else // 90 either way
+ {
+ // Rotate the canvas +-90 about the center
+ canvas.rotate(optimizedRotationSafe.val, canvas.getWidth()/2, canvas.getHeight()/2);
+
+ // Translate the canvas such that 0,0 is in the top left corner (for this perspective) ONSCREEN.
+ int origin_x = (canvas.getWidth()-canvas.getHeight())/2;
+ int origin_y = -origin_x;
+ canvas.translate(origin_x, origin_y);
+
+ // Now draw as normal, but, the onscreen width and height are swapped
+ unifiedDraw(canvas, canvas.getHeight(), canvas.getWidth(), userHook, userCtx);
+ }
+ }
+ }
+
+ public void setRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy policy)
+ {
+ renderingPolicy = policy;
+ }
+
+ public void renderPaused(Canvas canvas)
+ {
+ canvas.drawColor(PAUSED_COLOR);
+
+ Rect rect = createRect(
+ 0,
+ canvas.getHeight()-statBoxH,
+ statBoxW,
+ statBoxH
+ );
+
+ // Draw the purple rectangle
+ canvas.drawRect(rect, fpsMeterNormalBgPaint);
+
+ // Some formatting stuff
+ int statBoxLTxtStart = rect.left+statBoxLTxtMargin;
+ int textLine1Y = rect.bottom - statBoxTextFirstLineYFromBottomOffset;
+ int textLine2Y = textLine1Y + statBoxTextLineSpacing;
+ int textLine3Y = textLine2Y + statBoxTextLineSpacing;
+
+ // Draw the 3 text lines
+ canvas.drawText(fpsMeterDescriptor, statBoxLTxtStart, textLine1Y, fpsMeterTextPaint);
+ canvas.drawText("VIEWPORT PAUSED", statBoxLTxtStart, textLine2Y, fpsMeterTextPaint);
+ //canvas.drawText("Hi", statBoxLTxtStart, textLine3Y, fpsMeterTextPaint);
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java
new file mode 100644
index 00000000..9f49b525
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvViewport.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import android.graphics.Canvas;
+
+import org.opencv.core.Mat;
+
+public interface OpenCvViewport
+{
+ enum OptimizedRotation
+ {
+ NONE(0),
+ ROT_90_COUNTERCLOCWISE(90),
+ ROT_90_CLOCKWISE(-90),
+ ROT_180(180);
+
+ int val;
+
+ OptimizedRotation(int val)
+ {
+ this.val = val;
+ }
+ }
+
+ interface RenderHook
+ {
+ void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight, float scaleBmpPxToCanvasPx, float canvasDensityScale, Object userContext);
+ }
+
+ void setFpsMeterEnabled(boolean enabled);
+ void pause();
+ void resume();
+
+ void activate();
+ void deactivate();
+
+ void setSize(int width, int height);
+ void setOptimizedViewRotation(OptimizedRotation rotation);
+
+ void notifyStatistics(float fps, int pipelineMs, int overheadMs);
+ void setRecording(boolean recording);
+
+ void post(Mat frame, Object userContext);
+ void setRenderingPolicy(OpenCvCamera.ViewportRenderingPolicy policy);
+ void setRenderHook(RenderHook renderHook);
+
+ class FrameContext
+ {
+ public OpenCvPipeline generatingPipeline;
+ public Object userContext;
+
+ public FrameContext(OpenCvPipeline generatingPipeline, Object userContext)
+ {
+ this.generatingPipeline = generatingPipeline;
+ this.userContext = userContext;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java b/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java
new file mode 100644
index 00000000..924116cc
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/OpenCvWebcam.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2020 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+public interface OpenCvWebcam extends OpenCvCamera {
+
+ default void startStreaming(int width, int height, OpenCvCameraRotation cameraRotation, StreamFormat eocvStreamFormat) {
+ startStreaming(width, height, cameraRotation);
+ }
+
+ enum StreamFormat
+ {
+ // The only format that was supported historically; it is uncompressed but
+ // chroma subsampled and uses lots of bandwidth - this limits frame rate
+ // at higher resolutions and also limits the ability to use two cameras
+ // on the same bus to lower resolutions
+ YUY2,
+
+ // Compressed motion JPEG stream format; allows for higher resolutions at
+ // full frame rate, and better ability to use two cameras on the same bus.
+ // Requires extra CPU time to run decompression routine.
+ MJPEG;
+ }
+
+}
diff --git a/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java b/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java
new file mode 100644
index 00000000..646f9449
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/PipelineRecordingParameters.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2020 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class PipelineRecordingParameters
+{
+ public final String path;
+ public final Encoder encoder;
+ public final OutputFormat outputFormat;
+ public final int bitrate;
+ public final int frameRate;
+
+ public enum Encoder
+ {
+ H264(),
+ H263(),
+ VP8(),
+ MPEG_4_SP();
+ }
+
+ public enum OutputFormat
+ {
+ MPEG_4(),
+ THREE_GPP(),
+ WEBM();
+ }
+
+ public enum BitrateUnits
+ {
+ bps(1),
+ Kbps(1000),
+ Mbps(1000000);
+
+ final int scalar;
+
+ BitrateUnits(int scalar)
+ {
+ this.scalar = scalar;
+ }
+ }
+
+ public PipelineRecordingParameters(OutputFormat outputFormat, Encoder encoder, int frameRate, int bitrate, String path)
+ {
+ this.outputFormat = outputFormat;
+ this.encoder = encoder;
+ this.frameRate = frameRate;
+ this.bitrate = bitrate;
+ this.path = path;
+ }
+
+ public static class Builder
+ {
+ private String path = "/sdcard/EasyOpenCV/pipeline_recording_"+new SimpleDateFormat("dd-MM-yyyy_HH:mm:ss", Locale.getDefault()).format(new Date())+".mp4";
+ private Encoder encoder = Encoder.H264;
+ private OutputFormat outputFormat = OutputFormat.MPEG_4;
+ private int bitrate = 4000000;
+ private int frameRate = 30;
+
+ public Builder setPath(String path)
+ {
+ this.path = path;
+ return this;
+ }
+
+ public Builder setEncoder(Encoder encoder)
+ {
+ this.encoder = encoder;
+ return this;
+ }
+
+ public Builder setOutputFormat(OutputFormat outputFormat)
+ {
+ this.outputFormat = outputFormat;
+ return this;
+ }
+
+ public Builder setBitrate(int bitrate, BitrateUnits units)
+ {
+ this.bitrate = bitrate*units.scalar;
+ return this;
+ }
+
+ public Builder setFrameRate(int frameRate)
+ {
+ this.frameRate = frameRate;
+ return this;
+ }
+
+ public PipelineRecordingParameters build()
+ {
+ return new PipelineRecordingParameters(outputFormat, encoder, frameRate, bitrate, path);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java
new file mode 100644
index 00000000..67592c78
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/SourcedOpenCvCameraFactoryImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2023 Sebastian Erives
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+package org.openftc.easyopencv;
+
+import io.github.deltacv.vision.external.SourcedOpenCvCamera;
+import io.github.deltacv.vision.external.source.VisionSource;
+import io.github.deltacv.vision.external.source.ThreadSourceHander;
+import io.github.deltacv.vision.external.source.ViewportAndSourceHander;
+import io.github.deltacv.vision.internal.source.ftc.SourcedCameraName;
+import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
+
+public class SourcedOpenCvCameraFactoryImpl extends OpenCvCameraFactory {
+
+ private OpenCvViewport viewport() {
+ if(ThreadSourceHander.threadHander() instanceof ViewportAndSourceHander) {
+ return ((ViewportAndSourceHander) ThreadSourceHander.threadHander()).viewport();
+ }
+
+ return null;
+ }
+
+ private VisionSource source(String name) {
+ if(ThreadSourceHander.threadHander() instanceof ViewportAndSourceHander) {
+ return ThreadSourceHander.threadHander().hand(name);
+ }
+
+ return null;
+ }
+
+ @Override
+ public OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction) {
+ return new SourcedOpenCvCamera(source("default"), viewport(), false);
+ }
+
+ @Override
+ public OpenCvCamera createInternalCamera(OpenCvInternalCamera.CameraDirection direction, int viewportContainerId) {
+ if(viewportContainerId <= 0) {
+ return createInternalCamera(direction);
+ }
+
+ return new SourcedOpenCvCamera(source("default"), viewport(), true);
+ }
+
+ @Override
+ public OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction) {
+ return new SourcedOpenCvCamera(source("default"), viewport(), false);
+ }
+
+ @Override
+ public OpenCvCamera createInternalCamera2(OpenCvInternalCamera2.CameraDirection direction, int viewportContainerId) {
+ if(viewportContainerId <= 0) {
+ return createInternalCamera2(direction);
+ }
+
+ return new SourcedOpenCvCamera(source("default"), viewport(), true);
+ }
+
+ @Override
+ public OpenCvWebcam createWebcam(WebcamName cameraName) {
+ if(cameraName instanceof SourcedCameraName) {
+ return new SourcedOpenCvCamera(((SourcedCameraName) cameraName).getSource(), viewport(), false);
+ } else {
+ throw new IllegalArgumentException("cameraName is not compatible with SourcedOpenCvCamera");
+ }
+ }
+
+ @Override
+ public OpenCvWebcam createWebcam(WebcamName cameraName, int viewportContainerId) {
+ if(viewportContainerId <= 0) {
+ return createWebcam(cameraName);
+ }
+
+ if(cameraName instanceof SourcedCameraName) {
+ return new SourcedOpenCvCamera(((SourcedCameraName) cameraName).getSource(), viewport(), true);
+ } else {
+ throw new IllegalArgumentException("cameraName is not compatible with SourcedOpenCvCamera");
+ }
+ }
+
+}
diff --git a/Common/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java b/Vision/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java
similarity index 97%
rename from Common/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java
rename to Vision/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java
index e2453bae..7281d245 100644
--- a/Common/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java
+++ b/Vision/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java
@@ -1,42 +1,42 @@
-/*
- * Copyright (c) 2020 OpenFTC Team
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.openftc.easyopencv;
-
-import org.opencv.core.Mat;
-
-public abstract class TimestampedOpenCvPipeline extends OpenCvPipeline
-{
- private long timestamp;
-
- @Override
- public final Mat processFrame(Mat input)
- {
- return processFrame(input, timestamp);
- }
-
- public abstract Mat processFrame(Mat input, long captureTimeNanos);
-
- protected void setTimestamp(long timestamp)
- {
- this.timestamp = timestamp;
- }
+/*
+ * Copyright (c) 2020 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import org.opencv.core.Mat;
+
+public abstract class TimestampedOpenCvPipeline extends OpenCvPipeline
+{
+ private long timestamp;
+
+ @Override
+ public final Mat processFrame(Mat input)
+ {
+ return processFrame(input, timestamp);
+ }
+
+ public abstract Mat processFrame(Mat input, long captureTimeNanos);
+
+ protected void setTimestamp(long timestamp)
+ {
+ this.timestamp = timestamp;
+ }
}
\ No newline at end of file
diff --git a/Vision/src/main/java/org/openftc/easyopencv/Util.java b/Vision/src/main/java/org/openftc/easyopencv/Util.java
new file mode 100644
index 00000000..a8cf87a7
--- /dev/null
+++ b/Vision/src/main/java/org/openftc/easyopencv/Util.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2023 OpenFTC Team
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.openftc.easyopencv;
+
+import java.util.concurrent.CountDownLatch;
+
+public class Util
+{
+ public static void joinUninterruptibly(Thread thread)
+ {
+ boolean interrupted = false;
+
+ while (true)
+ {
+ try
+ {
+ thread.join();
+ break;
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ interrupted = true;
+ }
+ }
+
+ if (interrupted)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public static void acquireUninterruptibly(CountDownLatch latch)
+ {
+ boolean interrupted = false;
+
+ while (true)
+ {
+ try
+ {
+ latch.await();
+ break;
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ interrupted = true;
+ }
+ }
+
+ if (interrupted)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
\ No newline at end of file
diff --git a/build.common.gradle b/build.common.gradle
index 828f6c35..07f92ee5 100644
--- a/build.common.gradle
+++ b/build.common.gradle
@@ -1,14 +1,5 @@
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-}
-
-if (project.getPluginManager().hasPlugin("org.jetbrains.kotlin.jvm")) {
- compileKotlin {
- kotlinOptions {
- jvmTarget = "1.8"
- freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
- useIR = true
- }
- }
-}
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(10)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 785af733..8628c343 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,12 +4,13 @@ import java.time.format.DateTimeFormatter
buildscript {
ext {
- kotlin_version = "1.5.31"
+ kotlin_version = "1.9.0"
kotlinx_coroutines_version = "1.5.0-native-mt"
slf4j_version = "1.7.32"
log4j_version = "2.17.1"
opencv_version = "4.5.5-1"
- apriltag_plugin_version = "1.2.0"
+ apriltag_plugin_version = "2.0.0-B"
+ skiko_version = "0.7.75"
classgraph_version = "4.8.108"
opencsv_version = "5.5.2"
@@ -30,9 +31,15 @@ buildscript {
}
}
+plugins {
+ id 'java'
+}
+
allprojects {
group 'com.github.deltacv'
- version '3.4.3'
+ version '3.5.0'
+
+ apply plugin: 'java'
ext {
standardVersion = version
@@ -42,9 +49,12 @@ allprojects {
mavenCentral()
mavenLocal()
+ google()
+
maven { url "https://jitpack.io" }
maven { url 'https://maven.openimaj.org/' }
maven { url 'https://maven.ecs.soton.ac.uk/content/repositories/thirdparty/' }
+ maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
}
tasks.withType(Jar) {
@@ -72,4 +82,4 @@ allprojects {
file.delete()
}
}
-}
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b65adcfb..cf5dd7d2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/jitpack.yml b/jitpack.yml
index ce647a8b..bbf4c700 100644
--- a/jitpack.yml
+++ b/jitpack.yml
@@ -3,4 +3,4 @@ jdk:
before_install:
- chmod +x gradlew
install:
- - ./gradlew :EOCV-Sim:clean :EOCV-Sim:build :EOCV-Sim:publishToMavenLocal :Common:publishToMavenLocal -x :EOCV-Sim:test
+ - ./gradlew :EOCV-Sim:clean :EOCV-Sim:build :EOCV-Sim:publishToMavenLocal :Common:publishToMavenLocal :Vision:publishToMavenLocal -x :EOCV-Sim:test
diff --git a/settings.gradle b/settings.gradle
index 626bd667..3e05ebcd 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,9 +6,13 @@ pluginManagement {
}
}
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version "0.4.0"
+}
+
rootProject.name = 'EOCV-Sim'
include 'TeamCode'
include 'EOCV-Sim'
include 'Common'
-
+include 'Vision'
\ No newline at end of file