diff --git a/.github/workflows/build_ci.yml b/.github/workflows/build_ci.yml index 1e0ef849..7d086724 100644 --- a/.github/workflows/build_ci.yml +++ b/.github/workflows/build_ci.yml @@ -8,11 +8,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 8 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle @@ -23,11 +23,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 8 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle @@ -38,11 +38,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 8 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build and test with Gradle diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index d3425656..53dfb8eb 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 + - name: Set up JDK + uses: actions/setup-java@v3 with: - distribution: adopt - java-version: 8 + distribution: 'zulu' + java-version: '13' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/Common/build.gradle b/Common/build.gradle index cc150939..ba521628 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'java' id 'kotlin' id 'maven-publish' } @@ -22,6 +21,7 @@ publishing { dependencies { api "org.openpnp:opencv:$opencv_version" + implementation "org.slf4j:slf4j-api:$slf4j_version" - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation 'org.jetbrains.kotlin:kotlin-stdlib' } \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/AnyThread.java b/Common/src/main/java/android/annotation/AnyThread.java new file mode 100644 index 00000000..ff7a2b0f --- /dev/null +++ b/Common/src/main/java/android/annotation/AnyThread.java @@ -0,0 +1,44 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; +/** + * Denotes that the annotated method can be called from any thread (e.g. it is "thread safe".) + * If the annotated element is a class, then all methods in the class can be called + * from any thread. + *

+ * The main purpose of this method is to indicate that you believe a method can be called + * from any thread; static tools can then check that nothing you call from within this method + * or class have more strict threading requirements. + *

+ * Example: + *


+ *  @AnyThread
+ *  public void deliverResult(D data) { ... }
+ * 
+ */ +@Documented +@Retention(CLASS) +@Target({METHOD,CONSTRUCTOR,TYPE}) +public @interface AnyThread { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/ColorInt.java b/Common/src/main/java/android/annotation/ColorInt.java new file mode 100644 index 00000000..b378bade --- /dev/null +++ b/Common/src/main/java/android/annotation/ColorInt.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 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.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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; +/** + * Denotes that the annotated element represents a packed color + * int, {@code AARRGGBB}. If applied to an int array, every element + * in the array represents a color integer. + *

+ * Example: + *

{@code
+ *  public abstract void setTextColor(@ColorInt int color);
+ * }
+ */ +@Retention(CLASS) +@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD}) +public @interface ColorInt { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/ColorLong.java b/Common/src/main/java/android/annotation/ColorLong.java new file mode 100644 index 00000000..395fc468 --- /dev/null +++ b/Common/src/main/java/android/annotation/ColorLong.java @@ -0,0 +1,44 @@ +/* + * 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.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.SOURCE; +/** + *

Denotes that the annotated element represents a packed color + * long. If applied to a long array, every element in the array + * represents a color long. For more information on how colors + * are packed in a long, please refer to the documentation of + * the {link android.graphics.Color} class.

+ * + *

Example:

+ * + *
{@code
+ *  public void setFillColor(@ColorLong long color);
+ * }
+ * + * see android.graphics.Color + * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD}) +public @interface ColorLong { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/HalfFloat.java b/Common/src/main/java/android/annotation/HalfFloat.java new file mode 100644 index 00000000..3f25dc23 --- /dev/null +++ b/Common/src/main/java/android/annotation/HalfFloat.java @@ -0,0 +1,45 @@ +/* + * 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.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.SOURCE; +/** + *

Denotes that the annotated element represents a half-precision floating point + * value. Such values are stored in short data types and can be manipulated with + * the {@link android.util.Half} class. If applied to an array of short, every + * element in the array represents a half-precision float.

+ * + *

Example:

+ * + *
{@code
+ * public abstract void setPosition(@HalfFloat short x, @HalfFloat short y, @HalfFloat short z);
+ * }
+ * + * @see android.util.Half + * @see android.util.Half#toHalf(float) + * @see android.util.Half#toFloat(short) + * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) +public @interface HalfFloat { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/IntDef.java b/Common/src/main/java/android/annotation/IntDef.java new file mode 100644 index 00000000..2f425535 --- /dev/null +++ b/Common/src/main/java/android/annotation/IntDef.java @@ -0,0 +1,55 @@ +/* + * 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.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; +/** + * Denotes that the annotated element of integer type, represents + * a logical type and that its value should be one of the explicitly + * named constants. If the {@link #flag()} attribute is set to true, + * multiple constants can be combined. + *

+ *


+ *  @Retention(SOURCE)
+ *  @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ *  public @interface NavigationMode {}
+ *  public static final int NAVIGATION_MODE_STANDARD = 0;
+ *  public static final int NAVIGATION_MODE_LIST = 1;
+ *  public static final int NAVIGATION_MODE_TABS = 2;
+ *  ...
+ *  public abstract void setNavigationMode(@NavigationMode int mode);
+ *  @NavigationMode
+ *  public abstract int getNavigationMode();
+ * 
+ * For a flag, set the flag attribute: + *

+ *  @IntDef(
+ *      flag = true
+ *      value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * 
+ * + * @hide + */ +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface IntDef { + /** Defines the allowed constants for this element */ + long[] value() default {}; + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/IntRange.java b/Common/src/main/java/android/annotation/IntRange.java new file mode 100644 index 00000000..cf842f0b --- /dev/null +++ b/Common/src/main/java/android/annotation/IntRange.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 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.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; +/** + * Denotes that the annotated element should be an int or long in the given range + *

+ * Example: + *


+ *  @IntRange(from=0,to=255)
+ *  public int getAlpha() {
+ *      ...
+ *  }
+ * 
+ * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE}) +public @interface IntRange { + /** Smallest value, inclusive */ + long from() default Long.MIN_VALUE; + /** Largest value, inclusive */ + long to() default Long.MAX_VALUE; +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/NonNull.java b/Common/src/main/java/android/annotation/NonNull.java new file mode 100644 index 00000000..cdd06669 --- /dev/null +++ b/Common/src/main/java/android/annotation/NonNull.java @@ -0,0 +1,34 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * 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/android/annotation/Nullable.java b/Common/src/main/java/android/annotation/Nullable.java new file mode 100644 index 00000000..d6839263 --- /dev/null +++ b/Common/src/main/java/android/annotation/Nullable.java @@ -0,0 +1,45 @@ + +/* + * 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.annotation; +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; +// import android.annotation.SystemApi.Client; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * 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}) +// @SystemApi(client = Client.MODULE_LIBRARIES) +public @interface Nullable { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/Size.java b/Common/src/main/java/android/annotation/Size.java new file mode 100644 index 00000000..96d6f211 --- /dev/null +++ b/Common/src/main/java/android/annotation/Size.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 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.annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +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.SOURCE; +/** + * Denotes that the annotated element should have a given size or length. + * Note that "-1" means "unset". Typically used with a parameter or + * return value of type array or collection. + *

+ * Example: + *

{@code
+ *  public void getLocationInWindow(@Size(2) int[] location) {
+ *      ...
+ *  }
+ * }
+ * + * @hide + */ +@Retention(SOURCE) +@Target({PARAMETER,LOCAL_VARIABLE,METHOD,FIELD}) +public @interface Size { + /** An exact size (or -1 if not specified) */ + long value() default -1; + /** A minimum size, inclusive */ + long min() default Long.MIN_VALUE; + /** A maximum size, inclusive */ + long max() default Long.MAX_VALUE; + /** The size must be a multiple of this factor */ + long multiple() default 1; +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/SuppressAutoDoc.java b/Common/src/main/java/android/annotation/SuppressAutoDoc.java new file mode 100644 index 00000000..6c32a1ca --- /dev/null +++ b/Common/src/main/java/android/annotation/SuppressAutoDoc.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.annotation; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +/** + * Denotes that any automatically generated documentation should be suppressed + * for the annotated method, parameter, or field. + * + * @hide + */ +@Retention(SOURCE) +@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) +public @interface SuppressAutoDoc { +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/SuppressLint.java b/Common/src/main/java/android/annotation/SuppressLint.java new file mode 100644 index 00000000..09ed0c61 --- /dev/null +++ b/Common/src/main/java/android/annotation/SuppressLint.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 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.annotation; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +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.ElementType.TYPE; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** Indicates that Lint should ignore the specified warnings for the annotated element. */ +@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) +@Retention(RetentionPolicy.CLASS) +public @interface SuppressLint { + /** + * The set of warnings (identified by the lint issue id) that should be + * ignored by lint. It is not an error to specify an unrecognized name. + */ + String[] value(); +} \ No newline at end of file diff --git a/Common/src/main/java/android/annotation/SystemApi.java b/Common/src/main/java/android/annotation/SystemApi.java new file mode 100644 index 00000000..7c34de56 --- /dev/null +++ b/Common/src/main/java/android/annotation/SystemApi.java @@ -0,0 +1,74 @@ +/* + * 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 android.annotation; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Indicates an API is exposed for use by bundled system applications. + *

+ * These APIs are not guaranteed to remain consistent release-to-release, + * and are not for use by apps linking against the Android SDK. + *

+ * This annotation should only appear on API that is already marked

@hide
. + *

+ * + * @hide + */ +@Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(SystemApi.Container.class) // TODO(b/146727827): make this non-repeatable +public @interface SystemApi { + enum Client { + /** + * Specifies that the intended clients of a SystemApi are privileged apps. + * This is the default value for {@link #client}. + */ + PRIVILEGED_APPS, + /** + * Specifies that the intended clients of a SystemApi are used by classes in + *
BOOTCLASSPATH
in mainline modules. Mainline modules can also expose + * this type of system APIs too when they're used only by the non-updatable + * platform code. + */ + MODULE_LIBRARIES, + /** + * Specifies that the system API is available only in the system server process. + * Use this to expose APIs from code loaded by the system server process but + * not in
BOOTCLASSPATH
. + */ + SYSTEM_SERVER + } + /** + * The intended client of this SystemAPI. + */ + Client client() default android.annotation.SystemApi.Client.PRIVILEGED_APPS; + /** + * Container for {@link SystemApi} that allows it to be applied repeatedly to types. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(TYPE) + @interface Container { + SystemApi[] value(); + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/hardware/Camera.java b/Common/src/main/java/android/hardware/Camera.java new file mode 100644 index 00000000..09e43e5e --- /dev/null +++ b/Common/src/main/java/android/hardware/Camera.java @@ -0,0 +1,58 @@ +package android.hardware; + +public class Camera { + + + /** + * Information about a camera + */ + public static class CameraInfo { + /** + * The facing of the camera is opposite to that of the screen. + */ + public static final int CAMERA_FACING_BACK = 0; + /** + * The facing of the camera is the same as that of the screen. + */ + public static final int CAMERA_FACING_FRONT = 1; + /** + * The direction that the camera faces. It should be + * CAMERA_FACING_BACK or CAMERA_FACING_FRONT. + */ + public int facing; + /** + *

The orientation of the camera image. The value is the angle that the + * camera image needs to be rotated clockwise so it shows correctly on + * the display in its natural orientation. It should be 0, 90, 180, or 270.

+ * + *

For example, suppose a device has a naturally tall screen. The + * back-facing camera sensor is mounted in landscape. You are looking at + * the screen. If the top side of the camera sensor is aligned with the + * right edge of the screen in natural orientation, the value should be + * 90. If the top side of a front-facing camera sensor is aligned with + * the right of the screen, the value should be 270.

+ * + * see #setDisplayOrientation(int) + * see Parameters#setRotation(int) + * see Parameters#setPreviewSize(int, int) + * see Parameters#setPictureSize(int, int) + * see Parameters#setJpegThumbnailSize(int, int) + */ + public int orientation; + /** + *

Whether the shutter sound can be disabled.

+ * + *

On some devices, the camera shutter sound cannot be turned off + * through {link #enableShutterSound enableShutterSound}. This field + * can be used to determine whether a call to disable the shutter sound + * will succeed.

+ * + *

If this field is set to true, then a call of + * {@code enableShutterSound(false)} will be successful. If set to + * false, then that call will fail, and the shutter sound will be played + * when {link Camera#takePicture takePicture} is called.

+ */ + public boolean canDisableShutterSound; + }; + +} diff --git a/Common/src/main/java/android/hardware/DataSpace.java b/Common/src/main/java/android/hardware/DataSpace.java new file mode 100644 index 00000000..d0cf98cb --- /dev/null +++ b/Common/src/main/java/android/hardware/DataSpace.java @@ -0,0 +1,630 @@ +/* + * Copyright 2021 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.hardware; +import android.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +/** + * DataSpace identifies three components of colors - standard (primaries), transfer and range. + * + *

A DataSpace describes how buffer data, such as from an {link android.media.Image Image} + * or a {link android.hardware.HardwareBuffer HardwareBuffer} + * should be interpreted by both applications and typical hardware.

+ * + *

As buffer information is not guaranteed to be representative of color information, + * while DataSpace is typically used to describe three aspects of interpreting colors, + * some DataSpaces may describe other typical interpretations of buffer data + * such as depth information.

+ * + *

Note that while {link android.graphics.ColorSpace ColorSpace} and {@code DataSpace} + * are similar concepts, they are not equivalent. Not all ColorSpaces, + * such as {link android.graphics.ColorSpace.Named#ACES ColorSpace.Named.ACES}, + * are able to be understood by typical hardware blocks so they cannot be DataSpaces.

+ * + *

Standard aspect

+ * + *

Defines the chromaticity coordinates of the source primaries in terms of + * the CIE 1931 definition of x and y specified in ISO 11664-1.

+ * + *

Transfer aspect

+ * + *

Transfer characteristics are the opto-electronic transfer characteristic + * at the source as a function of linear optical intensity (luminance).

+ * + *

For digital signals, E corresponds to the recorded value. Normally, the + * transfer function is applied in RGB space to each of the R, G and B + * components independently. This may result in color shift that can be + * minized by applying the transfer function in Lab space only for the L + * component. Implementation may apply the transfer function in RGB space + * for all pixel formats if desired.

+ * + *

Range aspect

+ * + *

Defines the range of values corresponding to the unit range of {@code 0-1}.

+ */ +public final class DataSpace { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + STANDARD_UNSPECIFIED, + STANDARD_BT709, + STANDARD_BT601_625, + STANDARD_BT601_625_UNADJUSTED, + STANDARD_BT601_525, + STANDARD_BT601_525_UNADJUSTED, + STANDARD_BT2020, + STANDARD_BT2020_CONSTANT_LUMINANCE, + STANDARD_BT470M, + STANDARD_FILM, + STANDARD_DCI_P3, + STANDARD_ADOBE_RGB + }) + public @interface DataSpaceStandard {}; + private static final int STANDARD_MASK = 63 << 16; + /** + * Chromacity coordinates are unknown or are determined by the application. + */ + public static final int STANDARD_UNSPECIFIED = 0 << 16; + /** + * Use the unadjusted {@code KR = 0.2126}, {@code KB = 0.0722} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.300   0.600
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT709 = 1 << 16; + /** + * Use the adjusted {@code KR = 0.299}, {@code KB = 0.114} luminance interpretation + * for RGB conversion from the one purely determined by the primaries + * to minimize the color shift into RGB space that uses BT.709 + * primaries. + * + *
+     * Primaries:       x       y
+     *  green           0.290   0.600
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_625 = 2 << 16; + /** + * Use the unadjusted {@code KR = 0.222}, {@code KB = 0.071} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.290   0.600
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_625_UNADJUSTED = 3 << 16; + /** + * Use the adjusted {@code KR = 0.299}, {@code KB = 0.114} luminance interpretation + * for RGB conversion from the one purely determined by the primaries + * to minimize the color shift into RGB space that uses BT.709 + * primaries. + * + *
+     * Primaries:       x       y
+     *  green           0.310   0.595
+     *  blue            0.155   0.070
+     *  red             0.630   0.340
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_525 = 4 << 16; + /** + * Use the unadjusted {@code KR = 0.212}, {@code KB = 0.087} luminance interpretation + * for RGB conversion (as in SMPTE 240M). + * + *
+     * Primaries:       x       y
+     *  green           0.310   0.595
+     *  blue            0.155   0.070
+     *  red             0.630   0.340
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT601_525_UNADJUSTED = 5 << 16; + /** + * Use the unadjusted {@code KR = 0.2627}, {@code KB = 0.0593} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.170   0.797
+     *  blue            0.131   0.046
+     *  red             0.708   0.292
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT2020 = 6 << 16; + /** + * Use the unadjusted {@code KR = 0.2627}, {@code KB = 0.0593} luminance interpretation + * for RGB conversion using the linear domain. + * + *
+     * Primaries:       x       y
+     *  green           0.170   0.797
+     *  blue            0.131   0.046
+     *  red             0.708   0.292
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_BT2020_CONSTANT_LUMINANCE = 7 << 16; + /** + * Use the unadjusted {@code KR = 0.30}, {@code KB = 0.11} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x      y
+     *  green           0.21   0.71
+     *  blue            0.14   0.08
+     *  red             0.67   0.33
+     *  white (C)       0.310  0.316 
+ */ + public static final int STANDARD_BT470M = 8 << 16; + /** + * Use the unadjusted {@code KR = 0.254}, {@code KB = 0.068} luminance interpretation + * for RGB conversion. + * + *
+     * Primaries:       x       y
+     *  green           0.243   0.692
+     *  blue            0.145   0.049
+     *  red             0.681   0.319
+     *  white (C)       0.310   0.316 
+ */ + public static final int STANDARD_FILM = 9 << 16; + /** + * SMPTE EG 432-1 and SMPTE RP 431-2. + * + *
+     * Primaries:       x       y
+     *  green           0.265   0.690
+     *  blue            0.150   0.060
+     *  red             0.680   0.320
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_DCI_P3 = 10 << 16; + /** + * Adobe RGB primaries. + * + *
+     * Primaries:       x       y
+     *  green           0.210   0.710
+     *  blue            0.150   0.060
+     *  red             0.640   0.330
+     *  white (D65)     0.3127  0.3290 
+ */ + public static final int STANDARD_ADOBE_RGB = 11 << 16; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + TRANSFER_UNSPECIFIED, + TRANSFER_LINEAR, + TRANSFER_SRGB, + TRANSFER_SMPTE_170M, + TRANSFER_GAMMA2_2, + TRANSFER_GAMMA2_6, + TRANSFER_GAMMA2_8, + TRANSFER_ST2084, + TRANSFER_HLG + }) + public @interface DataSpaceTransfer {}; + private static final int TRANSFER_MASK = 31 << 22; + /** + * Transfer characteristics are unknown or are determined by the + * application. + */ + public static final int TRANSFER_UNSPECIFIED = 0 << 22; + /** + * Linear transfer. + * + *
{@code
+     * Transfer characteristic curve:
+     *  E = L
+     *      L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *      E - corresponding electrical signal}
+ */ + public static final int TRANSFER_LINEAR = 1 << 22; + /** + * sRGB transfer. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = 1.055 * L^(1/2.4) - 0.055  for 0.0031308 <= L <= 1
+     *   = 12.92 * L                  for 0 <= L < 0.0031308
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ * + * Use for RGB formats. + */ + public static final int TRANSFER_SRGB = 2 << 22; + /** + * SMPTE 170M transfer. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = 1.099 * L ^ 0.45 - 0.099  for 0.018 <= L <= 1
+     *   = 4.500 * L                 for 0 <= L < 0.018
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ * + * Use for YCbCr formats. + */ + public static final int TRANSFER_SMPTE_170M = 3 << 22; + /** + * Display gamma 2.2. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = L ^ (1/2.2)
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_GAMMA2_2 = 4 << 22; + /** + * Display gamma 2.6. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = L ^ (1/2.6)
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_GAMMA2_6 = 5 << 22; + /** + * Display gamma 2.8. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = L ^ (1/2.8)
+     *     L - luminance of image 0 <= L <= 1 for conventional colorimetry
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_GAMMA2_8 = 6 << 22; + /** + * SMPTE ST 2084 (Dolby Perceptual Quantizer). + * + *
{@code
+     * Transfer characteristic curve:
+     * E = ((c1 + c2 * L^n) / (1 + c3 * L^n)) ^ m
+     * c1 = c3 - c2 + 1 = 3424 / 4096 = 0.8359375
+     * c2 = 32 * 2413 / 4096 = 18.8515625
+     * c3 = 32 * 2392 / 4096 = 18.6875
+     * m = 128 * 2523 / 4096 = 78.84375
+     * n = 0.25 * 2610 / 4096 = 0.1593017578125
+     *     L - luminance of image 0 <= L <= 1 for HDR colorimetry.
+     *         L = 1 corresponds to 10000 cd/m2
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_ST2084 = 7 << 22; + /** + * ARIB STD-B67 Hybrid Log Gamma. + * + *
{@code
+     * Transfer characteristic curve:
+     * E = r * L^0.5                 for 0 <= L <= 1
+     *   = a * ln(L - b) + c         for 1 < L
+     * a = 0.17883277
+     * b = 0.28466892
+     * c = 0.55991073
+     * r = 0.5
+     *     L - luminance of image 0 <= L for HDR colorimetry. L = 1 corresponds
+     *         to reference white level of 100 cd/m2
+     *     E - corresponding electrical signal}
+ */ + public static final int TRANSFER_HLG = 8 << 22; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + RANGE_UNSPECIFIED, + RANGE_FULL, + RANGE_LIMITED, + RANGE_EXTENDED + }) + public @interface DataSpaceRange {}; + private static final int RANGE_MASK = 7 << 27; + /** + * Range characteristics are unknown or are determined by the application. + */ + public static final int RANGE_UNSPECIFIED = 0 << 27; + /** + * Full range uses all values for Y, Cb and Cr from + * {@code 0} to {@code 2^b-1}, where b is the bit depth of the color format. + */ + public static final int RANGE_FULL = 1 << 27; + /** + * Limited range uses values {@code 16/256*2^b} to {@code 235/256*2^b} for Y, and + * {@code 1/16*2^b} to {@code 15/16*2^b} for Cb, Cr, R, G and B, where b is the bit depth of + * the color format. + * + *

E.g. For 8-bit-depth formats: + * Luma (Y) samples should range from 16 to 235, inclusive + * Chroma (Cb, Cr) samples should range from 16 to 240, inclusive + * + * For 10-bit-depth formats: + * Luma (Y) samples should range from 64 to 940, inclusive + * Chroma (Cb, Cr) samples should range from 64 to 960, inclusive.

+ */ + public static final int RANGE_LIMITED = 2 << 27; + /** + * Extended range is used for scRGB only. + * + *

Intended for use with floating point pixel formats. [0.0 - 1.0] is the standard + * sRGB space. Values outside the range [0.0 - 1.0] can encode + * color outside the sRGB gamut. [-0.5, 7.5] is the scRGB range. + * Used to blend/merge multiple dataspaces on a single display.

+ */ + public static final int RANGE_EXTENDED = 3 << 27; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + DATASPACE_UNKNOWN, + DATASPACE_SCRGB_LINEAR, + DATASPACE_SRGB, + DATASPACE_SCRGB, + DATASPACE_DISPLAY_P3, + DATASPACE_BT2020_PQ, + DATASPACE_ADOBE_RGB, + DATASPACE_JFIF, + DATASPACE_BT601_625, + DATASPACE_BT601_525, + DATASPACE_BT2020, + DATASPACE_BT709, + DATASPACE_DCI_P3, + DATASPACE_SRGB_LINEAR + }) + public @interface NamedDataSpace {}; + /** + * Default-assumption data space, when not explicitly specified. + * + *

It is safest to assume a buffer is an image with sRGB primaries and + * encoding ranges, but the consumer and/or the producer of the data may + * simply be using defaults. No automatic gamma transform should be + * expected, except for a possible display gamma transform when drawn to a + * screen.

+ */ + public static final int DATASPACE_UNKNOWN = 0; + /** + * scRGB linear encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_LINEAR
+     *   Range: RANGE_EXTENDED
+ * + * The values are floating point. + * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. + * Values beyond the range [0.0 - 1.0] would correspond to other colors + * spaces and/or HDR content. + */ + public static final int DATASPACE_SCRGB_LINEAR = 406913024; + /** + * sRGB gamma encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_SRGB
+     *   Range: RANGE_FULL
+ * + * When written, the inverse transformation is performed. + * + * The alpha component, if present, is always stored in linear space and + * is left unmodified when read or written. + */ + public static final int DATASPACE_SRGB = 142671872; + /** + * scRGB gamma encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_SRGB
+     *   Range: RANGE_EXTENDED
+ * + * The values are floating point. + * + * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. + * Values beyond the range [0.0 - 1.0] would correspond to other colors + * spaces and/or HDR content. + */ + public static final int DATASPACE_SCRGB = 411107328; + /** + * Display P3 encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_DCI_P3
+     *   Transfer: TRANSFER_SRGB
+     *   Range: RANGE_FULL
+ */ + public static final int DATASPACE_DISPLAY_P3 = 143261696; + /** + * ITU-R Recommendation 2020 (BT.2020) + * + * Ultra High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT2020
+     *   Transfer: TRANSFER_ST2084
+     *   Range: RANGE_FULL
+ */ + public static final int DATASPACE_BT2020_PQ = 163971072; + /** + * Adobe RGB encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_ADOBE_RGB
+     *   Transfer: TRANSFER_GAMMA2_2
+     *   Range: RANGE_FULL
+ * + * Note: Application is responsible for gamma encoding the data. + */ + public static final int DATASPACE_ADOBE_RGB = 151715840; + /** + * JPEG File Interchange Format (JFIF). + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT601_625
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_FULL
+ * + * Same model as BT.601-625, but all values (Y, Cb, Cr) range from {@code 0} to {@code 255} + */ + public static final int DATASPACE_JFIF = 146931712; + /** + * ITU-R Recommendation 601 (BT.601) - 525-line + * + * Standard-definition television, 525 Lines (NTSC). + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT601_625
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_LIMITED
+ */ + public static final int DATASPACE_BT601_625 = 281149440; + /** + * ITU-R Recommendation 709 (BT.709) + * + * High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT601_525
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_LIMITED
+ */ + public static final int DATASPACE_BT601_525 = 281280512; + /** + * ITU-R Recommendation 2020 (BT.2020) + * + * Ultra High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT2020
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_FULL
+ */ + public static final int DATASPACE_BT2020 = 147193856; + /** + * ITU-R Recommendation 709 (BT.709) + * + * High-definition television. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_SMPTE_170M
+     *   Range: RANGE_LIMITED
+ */ + public static final int DATASPACE_BT709 = 281083904; + /** + * SMPTE EG 432-1 and SMPTE RP 431-2 + * + * Digital Cinema DCI-P3. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_DCI_P3
+     *   Transfer: TRANSFER_GAMMA2_6
+     *   Range: RANGE_FULL
+ * + * Note: Application is responsible for gamma encoding the data as + * a 2.6 gamma encoding is not supported in HW. + */ + public static final int DATASPACE_DCI_P3 = 155844608; + /** + * sRGB linear encoding. + * + *

Composed of the following -

+ *
+     *   Primaries: STANDARD_BT709
+     *   Transfer: TRANSFER_LINEAR
+     *   Range: RANGE_FULL
+ * + * The values are encoded using the full range ([0,255] for 8-bit) for all + * components. + */ + public static final int DATASPACE_SRGB_LINEAR = 138477568; + private DataSpace() {} + /** + * Pack the dataSpace value using standard, transfer and range field value. + * Field values should be in the correct bits place. + * + * @param standard Chromaticity coordinates of source primaries + * @param transfer Opto-electronic transfer characteristic at the source + * @param range The range of values + * + * @return The int dataspace packed by standard, transfer and range value + */ + public static @NamedDataSpace int pack(@DataSpaceStandard int standard, + @DataSpaceTransfer int transfer, + @DataSpaceRange int range) { + if ((standard & STANDARD_MASK) != standard) { + throw new IllegalArgumentException("Invalid standard " + standard); + } + if ((transfer & TRANSFER_MASK) != transfer) { + throw new IllegalArgumentException("Invalid transfer " + transfer); + } + if ((range & RANGE_MASK) != range) { + throw new IllegalArgumentException("Invalid range " + range); + } + return standard | transfer | range; + } + /** + * Unpack the standard field value from the packed dataSpace value. + * + * @param dataSpace The packed dataspace value + * + * @return The standard aspect + */ + public static @DataSpaceStandard int getStandard(@NamedDataSpace int dataSpace) { + @DataSpaceStandard int standard = dataSpace & STANDARD_MASK; + return standard; + } + /** + * Unpack the transfer field value from the packed dataSpace value + * + * @param dataSpace The packed dataspace value + * + * @return The transfer aspect + */ + public static @DataSpaceTransfer int getTransfer(@NamedDataSpace int dataSpace) { + @DataSpaceTransfer int transfer = dataSpace & TRANSFER_MASK; + return transfer; + } + /** + * Unpack the range field value from the packed dataSpace value + * + * @param dataSpace The packed dataspace value + * + * @return The range aspect + */ + public static @DataSpaceRange int getRange(@NamedDataSpace int dataSpace) { + @DataSpaceRange int range = dataSpace & RANGE_MASK; + return range; + } +} \ No newline at end of file diff --git a/Common/src/main/java/android/util/ArraySet.java b/Common/src/main/java/android/util/ArraySet.java new file mode 100644 index 00000000..97644c4b --- /dev/null +++ b/Common/src/main/java/android/util/ArraySet.java @@ -0,0 +1,903 @@ +/* + * 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 libcore.util.EmptyArray; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +/** + * ArraySet is a generic set data structure that is designed to be more memory efficient than a + * traditional {@link java.util.HashSet}. The design is very similar to + * {link ArrayMap}, with all of the caveats described there. This implementation is + * separate from ArrayMap, however, so the Object array contains only one item for each + * entry in the set (instead of a pair for a mapping). + * + *

Note that this implementation is not intended to be appropriate for data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashSet, 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%.

+ * + *

Because this container is intended to better balance memory use, unlike most other + * standard Java containers it will shrink its array as items are removed from it. Currently + * you have no control over this shrinking -- if you set a capacity and then remove an + * item, it may reduce the capacity to better match the current size. In the future an + * explicit call to set the capacity should turn off this aggressive shrinking behavior.

+ * + *

This structure is NOT thread-safe.

+ */ +public final class ArraySet implements Collection, Set { + private static final boolean DEBUG = false; + private static final String TAG = "ArraySet"; + /** + * The minimum amount by which the capacity of a ArraySet will increase. + * This is tuned to be relatively space-efficient. + */ + private static final int BASE_SIZE = 4; + /** + * Maximum number of entries to have in array caches. + */ + private static final int CACHE_SIZE = 10; + /** + * Caches of small array objects to avoid spamming garbage. The cache + * Object[] variable is a pointer to a linked list of array objects. + * The first entry in the array is a pointer to the next array in the + * list; the second entry is a pointer to the int[] hash code array for it. + */ + static Object[] sBaseCache; + static int sBaseCacheSize; + static Object[] sTwiceBaseCache; + static int sTwiceBaseCacheSize; + /** + * Separate locks for each cache since each can be accessed independently of the other without + * risk of a deadlock. + */ + private static final Object sBaseCacheLock = new Object(); + private static final Object sTwiceBaseCacheLock = new Object(); + private final boolean mIdentityHashCode; + + int[] mHashes; + + Object[] mArray; + + int mSize; + + private MapCollections mCollections; + + private int binarySearch(int[] hashes, int hash) { + try { + return ContainerHelpers.binarySearch(hashes, mSize, hash); + } catch (ArrayIndexOutOfBoundsException e) { + throw new ConcurrentModificationException(); + } + } + + private int indexOf(Object key, int hash) { + final int N = mSize; + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + int index = binarySearch(mHashes, hash); + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + // If the key at the returned index matches, that's what we want. + if (key.equals(mArray[index])) { + return index; + } + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == hash; end++) { + if (key.equals(mArray[end])) return end; + } + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { + if (key.equals(mArray[i])) return i; + } + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private int indexOfNull() { + final int N = mSize; + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + int index = binarySearch(mHashes, 0); + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + // If the key at the returned index matches, that's what we want. + if (null == mArray[index]) { + return index; + } + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == 0; end++) { + if (null == mArray[end]) return end; + } + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { + if (null == mArray[i]) return i; + } + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private void allocArrays(final int size) { + if (size == (BASE_SIZE * 2)) { + synchronized (sTwiceBaseCacheLock) { + if (sTwiceBaseCache != null) { + final Object[] array = sTwiceBaseCache; + try { + mArray = array; + sTwiceBaseCache = (Object[]) array[0]; + mHashes = (int[]) array[1]; + if (mHashes != null) { + array[0] = array[1] = null; + sTwiceBaseCacheSize--; + if (DEBUG) { + // Log.d(TAG, "Retrieving 2x cache " + Arrays.toString(mHashes) + // + " now have " + sTwiceBaseCacheSize + " entries"); + } + return; + } + } catch (ClassCastException e) { + } + // Whoops! Someone trampled the array (probably due to not protecting + // their access with a lock). Our cache is corrupt; report and give up. + //Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0] + // + " [1]=" + array[1]); + sTwiceBaseCache = null; + sTwiceBaseCacheSize = 0; + } + } + } else if (size == BASE_SIZE) { + synchronized (sBaseCacheLock) { + if (sBaseCache != null) { + final Object[] array = sBaseCache; + try { + mArray = array; + sBaseCache = (Object[]) array[0]; + mHashes = (int[]) array[1]; + if (mHashes != null) { + array[0] = array[1] = null; + sBaseCacheSize--; + if (DEBUG) { + //Log.d(TAG, "Retrieving 1x cache " + Arrays.toString(mHashes) + // + " now have " + sBaseCacheSize + " entries"); + } + return; + } + } catch (ClassCastException e) { + } + // Whoops! Someone trampled the array (probably due to not protecting + // their access with a lock). Our cache is corrupt; report and give up. + //Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0] + // + " [1]=" + array[1]); + sBaseCache = null; + sBaseCacheSize = 0; + } + } + } + mHashes = new int[size]; + mArray = new Object[size]; + } + /** + * Make sure NOT to call this method with arrays that can still be modified. In other + * words, don't pass mHashes or mArray in directly. + */ + private static void freeArrays(final int[] hashes, final Object[] array, final int size) { + if (hashes.length == (BASE_SIZE * 2)) { + synchronized (sTwiceBaseCacheLock) { + if (sTwiceBaseCacheSize < CACHE_SIZE) { + array[0] = sTwiceBaseCache; + array[1] = hashes; + for (int i = size - 1; i >= 2; i--) { + array[i] = null; + } + sTwiceBaseCache = array; + sTwiceBaseCacheSize++; + if (DEBUG) { + //Log.d(TAG, "Storing 2x cache " + Arrays.toString(array) + " now have " + // + sTwiceBaseCacheSize + " entries"); + } + } + } + } else if (hashes.length == BASE_SIZE) { + synchronized (sBaseCacheLock) { + if (sBaseCacheSize < CACHE_SIZE) { + array[0] = sBaseCache; + array[1] = hashes; + for (int i = size - 1; i >= 2; i--) { + array[i] = null; + } + sBaseCache = array; + sBaseCacheSize++; + if (DEBUG) { + //Log.d(TAG, "Storing 1x cache " + Arrays.toString(array) + " now have " + // + sBaseCacheSize + " entries"); + } + } + } + } + } + /** + * Create a new empty ArraySet. The default capacity of an array map is 0, and + * will grow once items are added to it. + */ + public ArraySet() { + this(0, false); + } + /** + * Create a new ArraySet with a given initial capacity. + */ + public ArraySet(int capacity) { + this(capacity, false); + } + /** {@hide} */ + public ArraySet(int capacity, boolean identityHashCode) { + mIdentityHashCode = identityHashCode; + if (capacity == 0) { + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + } else { + allocArrays(capacity); + } + mSize = 0; + } + /** + * Create a new ArraySet with the mappings from the given ArraySet. + */ + public ArraySet(ArraySet set) { + this(); + if (set != null) { + addAll(set); + } + } + /** + * Create a new ArraySet with items from the given collection. + */ + public ArraySet(Collection set) { + this(); + if (set != null) { + addAll(set); + } + } + /** + * Create a new ArraySet with items from the given array + */ + public ArraySet(@Nullable E[] array) { + this(); + if (array != null) { + for (E value : array) { + add(value); + } + } + } + /** + * Make the array map empty. All storage is released. + */ + @Override + public void clear() { + if (mSize != 0) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + final int osize = mSize; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + freeArrays(ohashes, oarray, osize); + } + if (mSize != 0) { + throw new ConcurrentModificationException(); + } + } + /** + * Ensure the array map can hold at least minimumCapacity + * items. + */ + public void ensureCapacity(int minimumCapacity) { + final int oSize = mSize; + if (mHashes.length < minimumCapacity) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(minimumCapacity); + if (mSize > 0) { + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize); + } + freeArrays(ohashes, oarray, mSize); + } + if (mSize != oSize) { + throw new ConcurrentModificationException(); + } + } + /** + * Check whether a value exists in the set. + * + * @param key The value to search for. + * @return Returns true if the value exists, else false. + */ + @Override + public boolean contains(Object key) { + return indexOf(key) >= 0; + } + /** + * Returns the index of a value in the set. + * + * @param key The value to search for. + * @return Returns the index of the value if it exists, else a negative integer. + */ + public int indexOf(Object key) { + return key == null ? indexOfNull() + : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode()); + } + /** + * Return the value at the given index in the array. + * + *

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.

+ * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public E 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 valueAtUnchecked(index); + } + /** + * Returns the value at the given index in the array without checking that the index is within + * bounds. This allows testing values at the end of the internal array, outside of the + * [0, mSize) bounds. + * + * @hide + */ + public E valueAtUnchecked(int index) { + return (E) mArray[index]; + } + /** + * Return true if the array map contains no items. + */ + @Override + public boolean isEmpty() { + return mSize <= 0; + } + /** + * Adds the specified object to this set. The set is not modified if it + * already contains the object. + * + * @param value the object to add. + * @return {@code true} if this set is modified, {@code false} otherwise. + */ + @Override + public boolean add(E value) { + final int oSize = mSize; + final int hash; + int index; + if (value == null) { + hash = 0; + index = indexOfNull(); + } else { + hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode(); + index = indexOf(value, hash); + } + if (index >= 0) { + return false; + } + index = ~index; + if (oSize >= mHashes.length) { + final int n = oSize >= (BASE_SIZE * 2) ? (oSize + (oSize >> 1)) + : (oSize >= BASE_SIZE ? (BASE_SIZE * 2) : BASE_SIZE); + //if (DEBUG) Log.d(TAG, "add: grow from " + mHashes.length + " to " + n); + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + if (oSize != mSize) { + throw new ConcurrentModificationException(); + } + if (mHashes.length > 0) { + //if (DEBUG) Log.d(TAG, "add: copy 0-" + oSize + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); + System.arraycopy(oarray, 0, mArray, 0, oarray.length); + } + freeArrays(ohashes, oarray, oSize); + } + if (index < oSize) { + if (DEBUG) { + //Log.d(TAG, "add: move " + index + "-" + (oSize - index) + " to " + (index + 1)); + } + System.arraycopy(mHashes, index, mHashes, index + 1, oSize - index); + System.arraycopy(mArray, index, mArray, index + 1, oSize - index); + } + if (oSize != mSize || index >= mHashes.length) { + throw new ConcurrentModificationException(); + } + mHashes[index] = hash; + mArray[index] = value; + mSize++; + return true; + } + /** + * Special fast path for appending items to the end of the array without validation. + * The array must already be large enough to contain the item. + * @hide + */ + public void append(E value) { + final int oSize = mSize; + final int index = mSize; + final int hash = value == null ? 0 + : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode()); + if (index >= mHashes.length) { + throw new IllegalStateException("Array is full"); + } + if (index > 0 && mHashes[index - 1] > hash) { + // Cannot optimize since it would break the sorted order - fallback to add() + if (DEBUG) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + //Log.w(TAG, "New hash " + hash + // + " is before end of array hash " + mHashes[index - 1] + // + " at index " + index, e); + } + add(value); + return; + } + if (oSize != mSize) { + throw new ConcurrentModificationException(); + } + mSize = index + 1; + mHashes[index] = hash; + mArray[index] = value; + } + /** + * Perform a {@link #add(Object)} of all values in array + * @param array The array whose contents are to be retrieved. + */ + public void addAll(ArraySet array) { + final int N = array.mSize; + ensureCapacity(mSize + N); + if (mSize == 0) { + if (N > 0) { + System.arraycopy(array.mHashes, 0, mHashes, 0, N); + System.arraycopy(array.mArray, 0, mArray, 0, N); + if (0 != mSize) { + throw new ConcurrentModificationException(); + } + mSize = N; + } + } else { + for (int i = 0; i < N; i++) { + add(array.valueAt(i)); + } + } + } + /** + * Removes the specified object from this set. + * + * @param object the object to remove. + * @return {@code true} if this set was modified, {@code false} otherwise. + */ + @Override + public boolean remove(Object object) { + final int index = indexOf(object); + if (index >= 0) { + removeAt(index); + return true; + } + return false; + } + /** Returns true if the array size should be decreased. */ + private boolean shouldShrink() { + return mHashes.length > (BASE_SIZE * 2) && mSize < mHashes.length / 3; + } + /** + * Returns the new size the array should have. Is only valid if {@link #shouldShrink} returns + * true. + */ + private int getNewShrunkenSize() { + // We don't allow it to shrink smaller than (BASE_SIZE*2) to avoid flapping between that + // and BASE_SIZE. + return mSize > (BASE_SIZE * 2) ? (mSize + (mSize >> 1)) : (BASE_SIZE * 2); + } + /** + * Remove the key/value mapping at the given 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.

+ * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public E removeAt(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); + } + final int oSize = mSize; + final Object old = mArray[index]; + if (oSize <= 1) { + // Now empty. + //if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); + clear(); + } else { + final int nSize = oSize - 1; + if (shouldShrink()) { + // Shrunk enough to reduce size of arrays. + final int n = getNewShrunkenSize(); + //if (DEBUG) //Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + if (index > 0) { + //if (DEBUG) //Log.d(TAG, "remove: copy from 0-" + index + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, index); + System.arraycopy(oarray, 0, mArray, 0, index); + } + if (index < nSize) { + if (DEBUG) { + //Log.d(TAG, "remove: copy from " + (index + 1) + "-" + nSize + // + " to " + index); + } + System.arraycopy(ohashes, index + 1, mHashes, index, nSize - index); + System.arraycopy(oarray, index + 1, mArray, index, nSize - index); + } + } else { + if (index < nSize) { + if (DEBUG) { + //Log.d(TAG, "remove: move " + (index + 1) + "-" + nSize + " to " + index); + } + System.arraycopy(mHashes, index + 1, mHashes, index, nSize - index); + System.arraycopy(mArray, index + 1, mArray, index, nSize - index); + } + mArray[nSize] = null; + } + if (oSize != mSize) { + throw new ConcurrentModificationException(); + } + mSize = nSize; + } + return (E) old; + } + /** + * Perform a {@link #remove(Object)} of all values in array + * @param array The array whose contents are to be removed. + */ + public boolean removeAll(ArraySet array) { + // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first + // pass, use the property that the sets are sorted by hash to make this linear passes + // (except for hash collisions, which means worst case still n*m), then do one + // collection pass into a new array. This avoids binary searches and excessive memcpy. + final int N = array.mSize; + // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all + // the single results, compare size before and after. + final int originalSize = mSize; + for (int i = 0; i < N; i++) { + remove(array.valueAt(i)); + } + return originalSize != mSize; + } + /** + * Removes all values that satisfy the predicate. This implementation avoids using the + * {@link #iterator()}. + * + * @param filter A predicate which returns true for elements to be removed + */ + @Override + public boolean removeIf(Predicate filter) { + if (mSize == 0) { + return false; + } + // Intentionally not using removeAt() to avoid unnecessary intermediate resizing. + int replaceIndex = 0; + int numRemoved = 0; + for (int i = 0; i < mSize; ++i) { + if (filter.test((E) mArray[i])) { + numRemoved++; + } else { + if (replaceIndex != i) { + mArray[replaceIndex] = mArray[i]; + mHashes[replaceIndex] = mHashes[i]; + } + replaceIndex++; + } + } + if (numRemoved == 0) { + return false; + } else if (numRemoved == mSize) { + clear(); + return true; + } + mSize -= numRemoved; + if (shouldShrink()) { + // Shrunk enough to reduce size of arrays. + final int n = getNewShrunkenSize(); + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize); + } else { + // Null out values at the end of the array. Not doing it in the loop above to avoid + // writing twice to the same index or writing unnecessarily if the array would have been + // discarded anyway. + for (int i = mSize; i < mArray.length; ++i) { + mArray[i] = null; + } + } + return true; + } + /** + * Return the number of items in this array map. + */ + @Override + public int size() { + return mSize; + } + /** + * Performs the given action for all elements in the stored order. This implementation overrides + * the default implementation to avoid using the {@link #iterator()}. + * + * @param action The action to be performed for each element + */ + @Override + public void forEach(Consumer action) { + if (action == null) { + throw new NullPointerException("action must not be null"); + } + for (int i = 0; i < mSize; ++i) { + action.accept(valueAt(i)); + } + } + @Override + public Object[] toArray() { + Object[] result = new Object[mSize]; + System.arraycopy(mArray, 0, result, 0, mSize); + return result; + } + @Override + public 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 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 startPrecision
01 ⁄ 16,777,216
1 ⁄ 16,3841 ⁄ 16,777,216
1 ⁄ 8,1921 ⁄ 8,388,608
1 ⁄ 4,0961 ⁄ 4,194,304
1 ⁄ 2,0481 ⁄ 2,097,152
1 ⁄ 1,0241 ⁄ 1,048,576
1 ⁄ 5121 ⁄ 524,288
1 ⁄ 2561 ⁄ 262,144
1 ⁄ 1281 ⁄ 131,072
1 ⁄ 641 ⁄ 65,536
1 ⁄ 321 ⁄ 32,768
1 ⁄ 161 ⁄ 16,384
1 ⁄ 81 ⁄ 8,192
1 ⁄ 41 ⁄ 4,096
1 ⁄ 21 ⁄ 2,048
11 ⁄ 1,024
21 ⁄ 512
41 ⁄ 256
81 ⁄ 128
161 ⁄ 64
321 ⁄ 32
641 ⁄ 16
1281 ⁄ 8
2561 ⁄ 4
5121 ⁄ 2
1,0241
2,0482
4,0964
8,1928
16,38416
32,76832
+ * + *

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> 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 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 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 indexth 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 indexth 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 startPrecision
01 ⁄ 16,777,216
1 ⁄ 16,3841 ⁄ 16,777,216
1 ⁄ 8,1921 ⁄ 8,388,608
1 ⁄ 4,0961 ⁄ 4,194,304
1 ⁄ 2,0481 ⁄ 2,097,152
1 ⁄ 1,0241 ⁄ 1,048,576
1 ⁄ 5121 ⁄ 524,288
1 ⁄ 2561 ⁄ 262,144
1 ⁄ 1281 ⁄ 131,072
1 ⁄ 641 ⁄ 65,536
1 ⁄ 321 ⁄ 32,768
1 ⁄ 161 ⁄ 16,384
1 ⁄ 81 ⁄ 8,192
1 ⁄ 41 ⁄ 4,096
1 ⁄ 21 ⁄ 2,048
11 ⁄ 1,024
21 ⁄ 512
41 ⁄ 256
81 ⁄ 128
161 ⁄ 64
321 ⁄ 32
641 ⁄ 16
1281 ⁄ 8
2561 ⁄ 4
5121 ⁄ 2
1,0241
2,0482
4,0964
8,1928
16,38416
32,76832
+ * + *

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 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%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 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 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 constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, Field.class, EOCVSim.class); - this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); + Constructor 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:

+ * + * + * + * + * + * + * + * + * + *
ComponentNameSizeRange
AAlpha8 bits\([0..255]\)
RRed8 bits\([0..255]\)
GGreen8 bits\([0..255]\)
BBlue8 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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ComponentNameSizeRange
{@link ColorSpace.Model#RGB RGB} color model
RRed16 bits\([-65504.0, 65504.0]\)
GGreen16 bits\([-65504.0, 65504.0]\)
BBlue16 bits\([-65504.0, 65504.0]\)
AAlpha10 bits\([0..1023]\)
Color space6 bits\([0..63]\)
{@link ColorSpace.Named#SRGB sRGB} color space
AAlpha8 bits\([0..255]\)
RRed8 bits\([0..255]\)
GGreen8 bits\([0..255]\)
BBlue8 bits\([0..255]\)
XUnused32 bits\(0\)
{@link ColorSpace.Model#XYZ XYZ} color model
XX16 bits\([-65504.0, 65504.0]\)
YY16 bits\([-65504.0, 65504.0]\)
ZZ16 bits\([-65504.0, 65504.0]\)
AAlpha10 bits\([0..1023]\)
Color space6 bits\([0..63]\)
{@link ColorSpace.Model#XYZ Lab} color model
LL16 bits\([-65504.0, 65504.0]\)
aa16 bits\([-65504.0, 65504.0]\)
bb16 bits\([-65504.0, 65504.0]\)
AAlpha10 bits\([0..1023]\)
Color space6 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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamesRGB IEC61966-2.1
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamesRGB IEC61966-2.1 (Linear)
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamescRGB-nl IEC 61966-2-2:2003
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NamescRGB IEC 61966-2-2:2003
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6400.3000.1500.3127
y0.3300.6000.0600.3290
PropertyValue
NameRec. ITU-R BT.709-5
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.7080.1700.1310.3127
y0.2920.7970.0460.3290
PropertyValue
NameRec. ITU-R BT.2020-1
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6800.2650.1500.314
y0.3200.6900.0600.351
PropertyValue
NameSMPTE RP 431-2-2007 DCI (P3)
CIE standard illuminantN/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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6800.2650.1500.3127
y0.3200.6900.0600.3290
PropertyValue
NameDisplay P3
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.670.210.140.310
y0.330.710.080.316
PropertyValue
NameNTSC (1953)
CIE standard illuminantC
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.6300.3100.1550.3127
y0.3400.5950.0700.3290
PropertyValue
NameSMPTE-C RGB
CIE standard illuminantD65
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).

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.640.210.150.3127
y0.330.710.060.3290
PropertyValue
NameAdobe RGB (1998)
CIE standard illuminantD65
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.73470.15960.03660.3457
y0.26530.84040.00010.3585
PropertyValue
NameROMM RGB ISO 22028-2:2013
CIE standard illuminantD50
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.734700.000000.000100.32168
y0.265301.00000-0.077000.33767
PropertyValue
NameSMPTE ST 2065-1:2012 ACES
CIE standard illuminantD60
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.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ChromaticityRedGreenBlueWhite point
x0.7130.1650.1280.32168
y0.2930.8300.0440.33767
PropertyValue
NameAcademy S-2014-004 ACEScg
CIE standard illuminantD60
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.

+ * + * + * + * + * + *
PropertyValue
NameGeneric XYZ
CIE standard illuminantD50
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.

+ * + * + * + * + * + *
PropertyValue
NameGeneric L*a*b*
CIE standard illuminantD50
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:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

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:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

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:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

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:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

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:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

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:

+ * + * + * + * + * + *
SpacePrimaries lengthWhite point length
xyY62
XYZ93
+ * + *

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