Compare commits

...

18 Commits

Author SHA1 Message Date
Matt
e6b0f398b6 Fix versioning picked up in CI (#291)
Fixes ignore blob in versioningHelper
2021-09-24 18:51:40 -04:00
Chris Gerth
5a475e1071 Fix crash in logging when log files are not writable (#286)
* Addresses null pointer crash in logging when log files are not writable

* One of these days, I'll be able to type code without spotless complaining.

But today is not that day.
2021-09-23 22:38:50 -04:00
Tyler Veness
f8def88e4d Rename tests to appease wpiformat (#290) 2021-09-23 21:13:04 -04:00
Chris Gerth
db09e5209f Colored shape fix (#288)
* Move test images out of resources folder

* Limit workers in CI

* Fix image area filtering bug in colored shape

* Add missing picam settings

* Swap to make blank/empty Mat when a picam doesn't supply a color image.

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-09-23 18:48:18 -04:00
Matt
9fdd945a52 Fix compilation with gradlew build (#284)
* Fix everything but test mode

* Update TestUtils.java

* Jank testutils fix

* Limit workers in CI
2021-09-07 06:49:07 -07:00
Matt
00b8e7d1c5 Add colored shape to the UI (#258)
* Fixup colored shape backend code

* More colored shape stuff

* Start adding shape change to drawing

* Mostly works!

* Add powercell image for shape test mode

* Make super-duper-sure to release stuff

Fixes colored shape leak

* Move approx poly dp into Contour

* Adjust epsilon threshold

* Add dialog to change pipeline type

* Run spotless

* Make yes red :>

* Move "no" button

* Fix duplication/deletion name logic and switching

* Fix compilation errors from rebase

* Update VisionSourceManager.java

* Update type dialog, remove duplicate popup

The dropdown still switches even if the user says "no" tho

Co-authored-by: Banks Troutman <btrout.dhrs@gmail.com>
2021-09-03 22:20:55 -04:00
Tyler Veness
798b8e398a Remove hasTargets member variable and fix docs warnings (#283)
hasTargets is redundant because the same information can be obtained by
checking the size of the targets array.
2021-09-03 22:19:38 -04:00
Tyler Veness
affb27038b Fix uninitialized variables in PhotonPipelineResult (#282)
If the default constructor is used, some member variables weren't properly initialized.
2021-09-02 20:48:05 -07:00
Tyler Veness
6767781a41 Update photonlib vendordep JSON URL (#281) 2021-09-01 05:11:03 -07:00
Matt
6007cc752d Add libpicam with gain slider bugfix (#278)
* Add libpicam with gain slider bugfix

* Patches to get zero-copy working with Pi3.

-- Success/Failure mistmatch assumptions - lots of functions in the JNI return true on failure (not true on success)
-- isVSCMSupported() is currently implemented to be "isVSCMNotSupported()"

Likely, we'll want at least some .so changes

Co-authored-by: Chris Gerth <chrisgerth010592@gmail.com>
2021-08-31 23:48:11 -04:00
Banks T
9cf5c77d69 CI test fix (#280)
Addresses OOM in CI
2021-08-31 20:27:51 -07:00
Matt
9dc5481d1c Push dev tag last 2021-04-02 17:04:22 -04:00
Matt
3948650e6c [photonlib] Fix C++ compilation errors (#266)
Properly builds linux athena artifacts and fixes vendor JSON bug
2021-03-28 14:36:03 -07:00
Chris Gerth
49fcdb64ed Update Sim Example Unit Conversions (#265)
Updated Java examples to fix radians/degrees mismatch.

Inspected c++ examples - they create a units::degree_t from the NT value, which should be handled properly inside the PhotonUtils methods.
2021-03-23 10:57:32 -07:00
Banks T
1e715ce4bb Sim target sorting and "easy" NT entries (#262)
* Typo fixes, add target sorting

* Add easy NT entries

* spotttttttttttttttttlessssssssssssss

* Run wpiformat

* Fix return on no targets

* formatting hell 2 electric boogaloo

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
2021-03-23 12:47:26 -04:00
Vasista Vovveti
f9fd7a0b45 Handle GetLatestResult segfault (#259)
* Handle GetLatestResult segfault

* Update PhotonCamera.cpp

* Update PhotonCamera.cpp
2021-03-21 16:29:29 -04:00
Matt
e9a3c2d1b8 Pull tags in photon CI (#263) 2021-03-15 16:13:45 -07:00
Banks T
129575dd23 Fix Test Mode (#246)
* Fix test mode path and args

* spoooootless

* Fix unit test resources
2021-03-07 21:39:02 -05:00
389 changed files with 1570 additions and 934 deletions

View File

@@ -72,11 +72,15 @@ jobs:
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew build -x check
./gradlew build -x check --max-workers 1
# Run Tests Generate Coverage Report.
- name: Gradle Test and Coverage
run: ./gradlew jacocoTestReport
# Run Gradle Tests.
- name: Gradle Tests
run: ./gradlew testHeadless -i --max-workers 1
# Generate Coverage Report.
- name: Gradle Coverage
run: ./gradlew jacocoTestReport --max-workers 1
# Publish Coverage Report.
- name: Publish Server Coverage Report
@@ -125,58 +129,6 @@ jobs:
name: built-docs
path: build/html
photon-build-package:
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs]
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Clear any existing web resources.
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-client
path: photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
with:
name: jar
path: photon-server/build/libs
- uses: eine/tip@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
if: github.event_name == 'push'
photonserver-check-lint:
# The type of runner that the job will run on.
runs-on: ubuntu-latest
@@ -195,6 +147,7 @@ jobs:
chmod +x gradlew
./gradlew spotlessCheck
photon-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [photon-build-package]
@@ -225,13 +178,16 @@ jobs:
runs-on: ${{ matrix.os }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish
name: Publish
env:
@@ -243,7 +199,7 @@ jobs:
fail-fast: false
matrix:
include:
- container: wpilib/roborio-cross-ubuntu:2020-18.04
- container: wpilib/roborio-cross-ubuntu:2021-18.04
artifact-name: Athena
- container: wpilib/raspbian-cross-ubuntu:10-18.04
artifact-name: Raspbian
@@ -254,13 +210,15 @@ jobs:
container: ${{ matrix.container }}
name: "Photonlib - Build - ${{ matrix.artifact-name }}"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
with:
java-version: 11
- run: |
chmod +x gradlew
./gradlew photon-lib:build
./gradlew photon-lib:build --max-workers 1
- run: |
chmod +x gradlew
./gradlew photon-lib:publish
@@ -287,8 +245,67 @@ jobs:
- name: Install wpiformat
run: pip3 install wpiformat
- name: Run
run: |
ls -la
wpiformat -clang 10 -f photon-lib
run: wpiformat -clang 10 -f photon-lib
- name: Check Output
run: git --no-pager diff --exit-code HEAD
- name: Generate diff
run: git diff HEAD > wpiformat-fixes.patch
if: ${{ failure() }}
- uses: actions/upload-artifact@v2
with:
name: wpiformat fixes
path: wpiformat-fixes.patch
if: ${{ failure() }}
photon-build-package:
needs: [photonclient-build, photon-build-all, photonserver-build-offline-docs, photonlib-build-host, photonlib-build-docker]
# The type of runner that the job will run on.
runs-on: ubuntu-latest
steps:
# Checkout code.
- uses: actions/checkout@v1
# Install Java 11.
- uses: actions/setup-java@v1
with:
java-version: 11
# Clear any existing web resources.
- run: |
rm -rf photon-server/src/main/resources/web/*
mkdir -p photon-server/src/main/resources/web/docs
# Download client artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-client
path: photon-server/src/main/resources/web/
# Download docs artifact to resources folder.
- uses: actions/download-artifact@v2
with:
name: built-docs
path: photon-server/src/main/resources/web/docs
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar --max-workers 1
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master
with:
name: jar
path: photon-server/build/libs
- uses: eine/tip@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
files: |
photon-server/build/libs/*.jar
if: github.event_name == 'push'

View File

@@ -22,7 +22,7 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
wpilibVersion = "2021.2.1"
wpilibVersion = "2021.3.1"
opencvVersion = "3.4.7-5"
joglVersion = "2.4.0-rc-20200307"
pubVersion = versionString

View File

@@ -15,6 +15,7 @@
:value="localValue"
:max="max"
:min="min"
:disabled="disabled"
hide-details
class="align-center"
dark
@@ -34,7 +35,7 @@
hide-details
single-line
type="number"
style="width: 50px"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="prependFocused = true"
@@ -53,7 +54,7 @@
hide-details
single-line
type="number"
style="width: 50px"
style="width: 60px"
:step="step"
@input="handleChange"
@focus="appendFocused = true"
@@ -75,7 +76,7 @@ export default {
TooltippedLabel,
},
// eslint-disable-next-line vue/require-prop-types
props: ["name", "min", "max", "value", "step", "tooltip"],
props: ["name", "min", "max", "value", "step", "tooltip", "disabled"],
data() {
return {
prependFocused: false,

View File

@@ -88,7 +88,11 @@
menu
</v-icon>
</template>
<v-list dense>
<v-list
dark
dense
color="primary"
>
<v-list-item @click="toPipelineNameChange">
<v-list-item-title>
<CVicon
@@ -119,7 +123,7 @@
/>
</v-list-item-title>
</v-list-item>
<v-list-item @click="openDuplicateDialog">
<v-list-item @click="duplicatePipeline">
<v-list-item-title>
<CVicon
color="#c5c5c5"
@@ -132,66 +136,51 @@
</v-list>
</v-menu>
</v-col>
<v-col
v-if="currentPipelineType >= 0"
cols="10"
md="5"
lg="10"
class="pt-0 pb-0 pl-6 ml-16"
>
<CVselect
v-model="currentPipelineType"
name="Type"
:list="['Reflective', 'Shape']"
@input="e => showTypeDialog(e)"
/>
</v-col>
</v-row>
<!--pipeline duplicate dialog-->
<v-dialog
v-model="duplicateDialog"
dark
width="500"
height="357"
>
<v-card dark>
<v-card-title
class="headline"
primary-title
>
Duplicate Pipeline
</v-card-title>
<v-card-text>
<CVselect
v-model="pipeIndexToDuplicate"
name="Pipeline"
:list="$store.getters.pipelineList"
/>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
color="#ffd843"
@click="duplicatePipeline"
>
Duplicate
</v-btn>
<v-btn
color="error"
@click="closeDuplicateDialog"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!--pipeline naming dialog-->
<v-dialog
v-model="namingDialog"
dark
persistent
width="500"
height="357"
>
<v-card dark>
<v-card
dark
color="primary"
>
<v-card-title
class="headline"
primary-title
>
Pipeline Name
{{ isPipelineNameEdit ? "Edit Pipeline Name" : "Create Pipeline" }}
</v-card-title>
<v-card-text>
<CVinput
v-model="newPipelineName"
name="Pipeline"
:error-message="checkPipelineName"
@Enter="savePipelineNameChange"
/>
<CVselect
v-model="newPipelineType"
name="Pipeline Type"
:list="['Reflective', 'Shape']"
:disabled="isPipelineNameEdit"
/>
</v-card-text>
<v-divider />
@@ -213,161 +202,205 @@
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="showPipeTypeDialog"
width="600"
>
<v-card
color="primary"
dark
>
<v-card-title>Change Pipeline Type</v-card-title>
<v-card-text>
Changing the type of this pipeline will erase the current pipeline's settings and replace it with a new {{ ['Reflective', 'Shape'][proposedPipelineType] }} pipeline. <b class="red--text format_bold">You will lose all settings for the pipeline
"{{ ($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)[currentPipelineIndex] }}."</b> Are you sure you want to do this?
<v-row
class="mt-6"
style="display: flex; align-items: center; justify-content: center"
align="center"
>
<v-btn
class="mr-3"
color="red"
width="250"
@click="e => changePipeType(true)"
>
Yes, replace this pipeline
</v-btn>
<v-btn
class="ml-10"
color="secondary"
width="250"
@click="e => changePipeType(false)"
>
No, take me back
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
duplicateDialog: false,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
}
},
methods: {
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
openDuplicateDialog() {
this.pipeIndexToDuplicate = this.currentPipelineIndex - 1;
this.duplicateDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline', {});
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", this.newPipelineName);
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
// if (!this.anotherCamera) {
// this.pipelineDuplicate.camera = -1
// }
this.handleInputWithIndex("duplicatePipeline", this.pipeIndexToDuplicate);
// this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipeIndexToDuplicate);
this.closeDuplicateDialog();
},
closeDuplicateDialog() {
this.duplicateDialog = false;
this.pipeIndexToDuplicate = undefined;
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
import CVicon from '../common/cv-icon'
import CVselect from '../common/cv-select'
import CVinput from '../common/cv-input'
export default {
name: "CameraAndPipelineSelect",
components: {
CVicon,
CVselect,
CVinput
},
data: () => {
return {
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
isCameraNameEdit: false,
newCameraName: "",
cameraNameError: "",
isPipelineNameEdit: false,
namingDialog: false,
newPipelineName: "",
newPipelineType: 0,
duplicateDialog: false,
showPipeTypeDialog: false,
proposedPipelineType : 0,
pipeIndexToDuplicate: undefined
}
},
computed: {
checkCameraName() {
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
if (this.re.test(this.newCameraName)) {
for (let cam in this.cameraList) {
if (this.cameraList.hasOwnProperty(cam)) {
if (this.newCameraName === this.cameraList[cam]) {
return "A camera by that name already Exists"
}
}
}
} else {
return "A camera name can only contain letters, numbers and spaces"
}
}
return "";
},
checkPipelineName() {
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
if (this.re.test(this.newPipelineName)) {
for (let pipe in this.$store.getters.pipelineList) {
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
return "A pipeline with this name already exists"
}
}
}
} else {
return "A pipeline name can only contain letters, numbers, and spaces"
}
}
return ""
},
currentCameraIndex: {
get() {
return this.$store.getters.currentCameraIndex;
},
set(value) {
this.$store.commit('currentCameraIndex', value);
}
},
currentPipelineIndex: {
get() {
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
},
set(value) {
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
}
},
currentPipelineType: {
get() {
return this.$store.getters.currentPipelineSettings.pipelineType - 2;
},
set(value) {
value; // nop, since we have the dialog for this
}
}
},
methods: {
showTypeDialog(idx) {
// Only show the dialog if it's a new type
this.showPipeTypeDialog = idx !== this.currentPipelineType;
this.proposedPipelineType = idx;
},
changePipeType(actuallyChange) {
const newIdx = actuallyChange ? this.proposedPipelineType : this.currentPipelineType
this.handleInputWithIndex('pipelineType', newIdx);
this.showPipeTypeDialog = false;
},
changeCameraName() {
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
this.isCameraNameEdit = true;
},
saveCameraNameChange() {
if (this.checkCameraName === "") {
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
// eslint-disable-next-line
.then(r => {
this.$emit('camera-name-changed')
})
.catch(e => {
console.log("HTTP error while changing camera name " + e);
this.$emit('camera-name-changed')
})
this.discardCameraNameChange();
}
},
discardCameraNameChange() {
this.isCameraNameEdit = false;
this.newCameraName = "";
},
toPipelineNameChange() {
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
this.isPipelineNameEdit = true;
this.namingDialog = true;
},
toCreatePipeline() {
this.newPipelineName = "New Pipeline";
this.isPipelineNameEdit = false;
this.namingDialog = true;
},
deleteCurrentPipeline() {
if (this.$store.getters.pipelineList.length > 1) {
this.handleInputWithIndex('deleteCurrentPipeline', {});
} else {
this.snackbar = true;
}
},
savePipelineNameChange() {
if (this.checkPipelineName === "") {
if (this.isPipelineNameEdit) {
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
} else {
this.handleInputWithIndex("addNewPipeline", [this.newPipelineName, this.newPipelineType]); // 0 for reflective, 1 for colored shpae
}
this.discardPipelineNameChange();
}
},
duplicatePipeline() {
this.handleInputWithIndex("duplicatePipeline", this.currentPipelineIndex);
},
discardPipelineNameChange() {
this.namingDialog = false;
this.isPipelineNameEdit = false;
this.newPipelineName = "";
},
}
}
</script>
<style scoped>

View File

@@ -29,9 +29,11 @@ Vue.use(VueAxios, axios);
Vue.prototype.$msgPack = msgPack(true);
import {dataHandleMixin} from './mixins/global/dataHandleMixin'
Vue.mixin(dataHandleMixin);
import {stateMixin} from './mixins/global/stateMixin'
Vue.mixin(stateMixin);
new Vue({
router,
store,

View File

@@ -0,0 +1,10 @@
export const stateMixin = {
methods: {
currentPipelineType() {
return this.$store.getters.pipelineType
},
currentPipelineSettings() {
return this.$store.getters.currentPipelineSettings
},
}
};

View File

@@ -42,7 +42,7 @@ export default new Vuex.Store({
isFovConfigurable: true,
calibrated: false,
currentPipelineSettings: {
pipelineType: 2, // One of "driver", "reflective", "shape"
pipelineType: 2, // One of "calib", "driver", "reflective", "shape"
// 2 is reflective
// Settings that apply to all pipeline types
@@ -245,5 +245,6 @@ export default new Vuex.Store({
},
pipelineList: state => state.cameraSettings[state.currentCameraIndex].pipelineNicknames,
calibrationList: state => state.cameraSettings[state.currentCameraIndex].calibrations,
pipelineType: state => state.cameraSettings[state.currentCameraIndex].currentPipelineSettings.pipelineType
}
})

View File

@@ -1,160 +1,286 @@
<template>
<div>
<CVrangeSlider
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourArea')"
@rollback="e=> rollback('contourArea',e)"
v-model="contourArea"
name="Area"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourArea')"
/>
<CVrangeSlider
v-model="contourRatio"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
@rollback="e=> rollback('contourRatio',e)"
v-model="contourRatio"
v-if="currentPipelineType() !== 3"
name="Ratio (W/H)"
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
min="0"
max="100"
step="0.1"
@input="handlePipelineData('contourRatio')"
/>
<CVrangeSlider
v-model="contourFullness"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
@rollback="e=> rollback('contourFullness',e)"
v-model="contourFullness"
v-if="currentPipelineType() !== 3"
name="Fullness"
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
min="0"
max="100"
@input="handlePipelineData('contourFullness')"
/>
<CVrangeSlider
v-model="contourPerimeter"
v-if="currentPipelineType() === 3"
name="Perimeter"
tooltip="Min and max perimeter of the shape, in pixels"
min="0"
max="4000"
@input="handlePipelineData('contourPerimeter')"
/>
<CVslider
v-model="contourSpecklePercentage"
name="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
@rollback="e=> rollback('contourSpecklePercentage',e)"
v-model="contourSpecklePercentage"
name="Speckle Rejection"
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('contourSpecklePercentage')"
/>
<template v-if="currentPipelineType() !== 3">
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual']"
@input="handlePipelineData('contourGroupingMode')"
/>
<CVselect
v-model="contourIntersection"
name="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
/>
</template>
<!-- If we arent not a shape, we are a shape-->
<template v-else>
<v-divider class="mt-3"/>
<CVselect
v-model="contourShape"
name="Target Shape"
tooltip="The shape of targets to look for"
:select-cols="largeBox"
:list="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
@input="handlePipelineData('contourShape')"
/>
<!-- Accuracy % is only for polygons-->
<CVslider
v-model="accuracyPercentage"
:disabled="currentPipelineSettings().contourShape < 1"
name="Shape Simplification"
tooltip="How much we should simply the input contour before checking how many sides it has"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('accuracyPercentage')"
/>
<!-- Similarly, the threshold is only for circles -->
<CVslider
v-model="circleDetectThreshold"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle match distance"
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleDetectThreshold')"
/>
<CVrangeSlider
v-model="contourRadius"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Radius"
min="0"
max="100"
step="1"
@input="handlePipelineData('contourRadius')"
/>
<CVslider
v-model="maxCannyThresh"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Max Canny Threshold"
min="1"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('maxCannyThresh')"
/>
<CVslider
v-model="circleAccuracy"
:disabled="currentPipelineSettings().contourShape !== 0"
name="Circle Accuracy"
min="0"
max="100"
:slider-cols="largeBox"
@input="handlePipelineData('circleAccuracy')"
/>
<v-divider class="mt-3"/>
</template>
<CVselect
v-model="contourGroupingMode"
name="Target Grouping"
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
:select-cols="largeBox"
:list="['Single','Dual']"
@input="handlePipelineData('contourGroupingMode')"
@rollback="e=> rollback('contourGroupingMode',e)"
/>
<CVselect
v-model="contourIntersection"
name="Target Intersection"
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
:select-cols="largeBox"
:list="['None','Up','Down','Left','Right']"
:disabled="contourGroupingMode === 0"
@input="handlePipelineData('contourIntersection')"
@rollback="e=> rollback('contourIntersection',e)"
/>
<CVselect
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
v-model="contourSortMode"
name="Target Sort"
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
:select-cols="largeBox"
:list="['Largest','Smallest','Highest','Lowest','Rightmost','Leftmost','Centermost']"
@input="handlePipelineData('contourSortMode')"
@rollback="e => rollback('contourSortMode', e)"
/>
</div>
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVselect from '../../components/common/cv-select'
import CVslider from '../../components/common/cv-slider'
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
export default {
name: 'Contours',
components: {
CVrangeSlider,
CVselect,
CVslider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
}
},
methods: {},
}
data() {
return {}
},
computed: {
largeBox: {
get() {
// Sliders and selectors should be fuller width if we're on screen size medium and
// up and either not in compact mode (because the tab will be 100% screen width),
// or in driver mode (where the card will also be 100% screen width).
return this.$vuetify.breakpoint.mdAndUp && (!this.$store.state.compactMode || this.$store.getters.isDriverMode) ? 10 : 8;
}
},
contourArea: {
get() {
return this.$store.getters.currentPipelineSettings.contourArea
},
set(val) {
this.$store.commit("mutatePipeline", {"contourArea": val});
}
},
contourRatio: {
get() {
return this.$store.getters.currentPipelineSettings.contourRatio
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRatio": val});
}
},
contourFullness: {
get() {
return this.$store.getters.currentPipelineSettings.contourFullness
},
set(val) {
this.$store.commit("mutatePipeline", {"contourFullness": val});
}
},
contourPerimeter: {
get() {
return this.$store.getters.currentPipelineSettings.contourPerimeter
},
set(val) {
this.$store.commit("mutatePipeline", {"contourPerimeter": val});
}
},
contourSpecklePercentage: {
get() {
return this.$store.getters.currentPipelineSettings.contourSpecklePercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSpecklePercentage": val});
}
},
contourGroupingMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourGroupingMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourGroupingMode": val});
}
},
contourSortMode: {
get() {
return this.$store.getters.currentPipelineSettings.contourSortMode
},
set(val) {
this.$store.commit("mutatePipeline", {"contourSortMode": val});
}
},
contourShape: {
get() {
return this.$store.getters.currentPipelineSettings.contourShape
},
set(val) {
this.$store.commit("mutatePipeline", {"contourShape": val});
}
},
contourIntersection: {
get() {
return this.$store.getters.currentPipelineSettings.contourIntersection
},
set(val) {
this.$store.commit("mutatePipeline", {"contourIntersection": val});
}
},
accuracyPercentage: {
get() {
return this.$store.getters.currentPipelineSettings.accuracyPercentage
},
set(val) {
this.$store.commit("mutatePipeline", {"accuracyPercentage": val});
}
},
contourRadius: {
get() {
return this.$store.getters.currentPipelineSettings.contourRadius
},
set(val) {
this.$store.commit("mutatePipeline", {"contourRadius": val});
}
},
circleDetectThreshold: {
get() {
return this.$store.getters.currentPipelineSettings.circleDetectThreshold
},
set(val) {
this.$store.commit("mutatePipeline", {"circleDetectThreshold": val});
}
},
maxCannyThresh: {
get() {
return this.$store.getters.currentPipelineSettings.maxCannyThresh
},
set(val) {
this.$store.commit("mutatePipeline", {"maxCannyThresh": val});
}
},
circleAccuracy: {
get() {
return this.$store.getters.currentPipelineSettings.circleAccuracy
},
set(val) {
this.$store.commit("mutatePipeline", {"circleAccuracy": val});
}
},
},
methods: {},
}
</script>
<style lang="" scoped>

View File

@@ -1,48 +1,66 @@
<template>
<div>
<CVrangeSlider
v-model="hsvHue"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
v-model="hsvHue"
name="Hue"
tooltip="Describes color"
:min="0"
:max="180"
@input="handlePipelineData('hsvHue')"
@rollback="e => rollback('hue',e)"
/>
<CVrangeSlider
v-model="hsvSaturation"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
v-model="hsvSaturation"
name="Saturation"
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvSaturation')"
@rollback="e => rollback('saturation',e)"
/>
<CVrangeSlider
v-model="hsvValue"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
v-model="hsvValue"
name="Value"
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
:min="0"
:max="255"
@input="handlePipelineData('hsvValue')"
@rollback="e => rollback('value',e)"
/>
<template v-if="this.currentPipelineType() === 3">
<CVSwitch
v-model="erode"
name="Erode"
tooltip="Removes pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('erode')"
@rollback="e => rollback('erode',e)"
/>
<CVSwitch
v-model="dilate"
class="mb-0"
name="Dilate"
tooltip="Adds pixels around the edges of white areas in the thresholded image"
@input="handlePipelineData('dilate')"
@rollback="e => rollback('dilate',e)"
/>
</template>
<v-divider class="mb-3 mt-3"/>
<div class="pt-3 white--text">
Color Picker
</div>
<v-divider
class="mt-3"
class="mt-3"
/>
<v-row
justify="center"
class="mt-3 mb-3"
justify="center"
class="mt-3 mb-3"
>
<template v-if="!$store.state.colorPicking">
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(3)"
>
<v-icon left>
mdi-minus
@@ -50,10 +68,10 @@
Shrink Range
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(1)"
>
<v-icon left>
mdi-plus-minus
@@ -61,10 +79,10 @@
Set To Average
</v-btn>
<v-btn
color="accent"
class="ma-2 black--text"
small
@click="setFunction(2)"
color="accent"
class="ma-2 black--text"
small
@click="setFunction(2)"
>
<v-icon left>
mdi-plus
@@ -74,11 +92,11 @@
</template>
<template v-else>
<v-btn
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
color="accent"
class="ma-2 black--text"
style="width: 30%;"
small
@click="setFunction(0)"
>
Cancel
</v-btn>
@@ -88,112 +106,130 @@
</template>
<script>
import CVrangeSlider from '../../components/common/cv-range-slider'
export default {
name: 'Threshold',
components: {
CVrangeSlider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val})
}
}
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
import CVrangeSlider from '../../components/common/cv-range-slider'
import CVSwitch from "@/components/common/cv-switch";
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
this.handlePipelineUpdate("inputShouldShow", true);
}
}
export default {
name: 'Threshold',
components: {
CVSwitch,
CVrangeSlider
},
// eslint-disable-next-line vue/require-prop-types
props: ['value'],
data() {
return {
currentFunction: undefined,
colorPicker: undefined,
showThresholdState: 0
}
},
computed: {
hsvHue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvHue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvHue": val})
}
},
hsvSaturation: {
get() {
return this.$store.getters.currentPipelineSettings.hsvSaturation
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvSaturation": val})
}
},
hsvValue: {
get() {
return this.$store.getters.currentPipelineSettings.hsvValue
},
set(val) {
this.$store.commit("mutatePipeline", {"hsvValue": val})
}
},
erode: {
get() {
return this.$store.getters.currentPipelineSettings.erode
},
set(val) {
this.$store.commit("mutatePipeline", {"erode": val});
}
},
dilate: {
get() {
return this.$store.getters.currentPipelineSettings.dilate
},
set(val) {
this.$store.commit("mutatePipeline", {"dilate": val});
}
},
},
mounted: function () {
const self = this;
this.colorPicker = require('../../plugins/ColorPicker').default;
this.$nextTick(() => {
self.colorPicker.initColorPicker();
});
},
methods: {
onClick(event) {
if (this.currentFunction !== undefined) {
this.colorPicker.initColorPicker();
let s = this.$store.getters.currentPipelineSettings;
let hsvArray = this.colorPicker.colorPickerClick(event, this.currentFunction,
[
[s.hsvHue[0], s.hsvSaturation[0], s.hsvValue[0]],
[s.hsvHue[1], s.hsvSaturation[1], s.hsvValue[1]]
].map(hsv => hsv.map(it => it || 0)));
// That `map` calls are to make sure that we don't let any undefined/null values slip in
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
s.hsvHue = [hsvArray[0][0], hsvArray[1][0]];
s.hsvSaturation = [hsvArray[0][1], hsvArray[1][1]];
s.hsvValue = [hsvArray[0][2], hsvArray[1][2]];
let msg = this.$msgPack.encode({
"changePipelineSetting": {
'hsvHue': s.hsvHue,
'hsvSaturation': s.hsvSaturation,
'hsvValue': s.hsvValue,
'cameraIndex': this.$store.state.currentCameraIndex
}
});
this.$socket.send(msg);
this.$emit('update');
}
},
setFunction(index) {
switch (index) {
case 0:
this.currentFunction = undefined;
this.$store.state.colorPicking = false;
this.handlePipelineUpdate("outputShouldDraw", true);
return;
case 1:
this.currentFunction = this.colorPicker.eyeDrop;
break;
case 2:
this.currentFunction = this.colorPicker.expand;
break;
case 3:
this.currentFunction = this.colorPicker.shrink;
break;
}
this.$store.state.colorPicking = true;
this.handlePipelineUpdate("outputShouldDraw", false);
this.$store.commit("mutatePipeline", {"inputShouldShow": true});
this.handlePipelineUpdate("inputShouldShow", true);
}
}
}
</script>

View File

@@ -344,6 +344,8 @@ public class Logger {
wantsFlush = true;
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e) {
// Nothing to do - no stream available for writing
}
}
}

View File

@@ -135,7 +135,7 @@ public class TestUtils {
}
private static Path getResourcesFolderPath(boolean testMode) {
return Path.of("src", (testMode ? "main" : "test"), "resources").toAbsolutePath();
return Path.of(testMode ? "src/main/resources" : "../test-resources").toAbsolutePath();
}
public static Path getTestMode2019ImagePath() {

View File

@@ -85,7 +85,7 @@ public class PicamJNI {
}
public static boolean isSupported() {
return libraryLoaded && isVCSMSupported() && getSensorModel() != SensorModel.Disconnected;
return libraryLoaded && !isVCSMSupported() && getSensorModel() != SensorModel.Disconnected;
}
public static SensorModel getSensorModel() {

View File

@@ -35,13 +35,17 @@ public class FileVisionSource extends VisionSource {
public FileVisionSource(CameraConfiguration cameraConfiguration) {
super(cameraConfiguration);
var calibration =
cameraConfiguration.calibrations.size() > 0
? cameraConfiguration.calibrations.get(0)
: null;
frameProvider =
new FileFrameProvider(
Path.of(cameraConfiguration.path),
cameraConfiguration.FOV,
FileFrameProvider.MAX_FPS,
cameraConfiguration.camPitch,
cameraConfiguration.calibrations.get(0));
calibration);
settables =
new FileSourceSettables(cameraConfiguration, frameProvider.get().frameStaticProperties);
}

View File

@@ -136,22 +136,22 @@ public class ZeroCopyPicamSource extends VisionSource {
@Override
public void setExposure(double exposure) {
lastExposure = exposure;
var success = PicamJNI.setExposure((int) Math.round(exposure));
if (!success) logger.warn("Couldn't set Pi camera exposure");
var failure = PicamJNI.setExposure((int) Math.round(exposure));
if (failure) logger.warn("Couldn't set Pi camera exposure");
}
@Override
public void setBrightness(int brightness) {
lastBrightness = brightness;
var success = PicamJNI.setBrightness(brightness);
if (!success) logger.warn("Couldn't set Pi camera brightness");
var failure = PicamJNI.setBrightness(brightness);
if (failure) logger.warn("Couldn't set Pi camera brightness");
}
@Override
public void setGain(int gain) {
lastGain = gain;
var success = PicamJNI.setGain(gain);
if (!success) logger.warn("Couldn't set Pi camera gain");
var failure = PicamJNI.setGain(gain);
if (failure) logger.warn("Couldn't set Pi camera gain");
}
@Override
@@ -162,12 +162,12 @@ public class ZeroCopyPicamSource extends VisionSource {
@Override
protected void setVideoModeInternal(VideoMode videoMode) {
var mode = (FPSRatedVideoMode) videoMode;
var success = PicamJNI.destroyCamera();
if (!success)
var failure = PicamJNI.destroyCamera();
if (failure)
throw new RuntimeException(
"Couldn't destroy a zero copy Pi camera while switching video modes");
success = PicamJNI.createCamera(mode.width, mode.height, mode.fpsActual);
if (!success)
failure = PicamJNI.createCamera(mode.width, mode.height, mode.fpsActual);
if (failure)
throw new RuntimeException(
"Couldn't create a zero copy Pi camera while switching video modes");

View File

@@ -35,10 +35,10 @@ public class AcceleratedPicamFrameProvider implements FrameProvider {
this.settables = visionSettables;
var vidMode = settables.getCurrentVideoMode();
var success = PicamJNI.createCamera(vidMode.width, vidMode.height, vidMode.fps);
if (!success) {
success = PicamJNI.destroyCamera();
if (!success) throw new RuntimeException("Couldn't destroy Pi camera after init failure!");
var failure = PicamJNI.createCamera(vidMode.width, vidMode.height, vidMode.fps);
if (failure) {
failure = PicamJNI.destroyCamera();
if (failure) throw new RuntimeException("Couldn't destroy Pi camera after init failure!");
throw new RuntimeException(
"Couldn't initialize zero copy Pi camera; check stdout for native code logs");
}

View File

@@ -47,7 +47,7 @@ public class CVMat implements Releasable {
private StringBuilder getStackTraceBuilder() {
var trace = Thread.currentThread().getStackTrace();
final int STACK_FRAMES_TO_SKIP = 4;
final int STACK_FRAMES_TO_SKIP = 3;
final var traceStr = new StringBuilder();
for (int idx = STACK_FRAMES_TO_SKIP; idx < trace.length; idx++) {
traceStr.append("\t\n").append(trace[idx]);

View File

@@ -17,13 +17,18 @@
package org.photonvision.vision.opencv;
import org.jetbrains.annotations.Nullable;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.MatOfPoint3f;
import org.opencv.imgproc.Imgproc;
import org.opencv.core.Point;
public class CVShape {
public class CVShape implements Releasable {
public final Contour contour;
public final ContourShape shape;
@Nullable public final ContourShape shape;
public double radius = 0;
public Point center = null;
private MatOfPoint3f customTarget = null;
@@ -34,6 +39,12 @@ public class CVShape {
this.shape = shape;
}
public CVShape(Contour contour, Point center, double radius) {
this(contour, ContourShape.Circle);
this.radius = radius;
this.center = center;
}
public CVShape(Contour contour, MatOfPoint3f targetPoints) {
this.contour = contour;
this.shape = ContourShape.Custom;
@@ -44,36 +55,10 @@ public class CVShape {
return contour;
}
public MatOfPoint2f getApproxPolyDp(double epsilon, boolean closed) {
@Override
public void release() {
if (customTarget != null) customTarget.release();
approxCurve.release();
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contour.getMat2f(), approxCurve, epsilon, closed);
return approxCurve;
}
public MatOfPoint2f getApproxPolyDpConvex(double epsilon, boolean closed) {
approxCurve.release();
approxCurve = new MatOfPoint2f();
Imgproc.approxPolyDP(contour.getConvexHull(), approxCurve, epsilon, closed);
return approxCurve;
}
boolean approxPolyMatchesShape() {
var pointList = approxCurve.toList();
// TODO: @Matt
switch (shape) {
case Custom:
break;
case Circle:
break;
case Triangle:
break;
case Quadrilateral:
break;
}
return true;
contour.release();
}
}

View File

@@ -18,6 +18,7 @@
package org.photonvision.vision.opencv;
import java.util.Comparator;
import org.jetbrains.annotations.Nullable;
import org.opencv.core.CvType;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfPoint;
@@ -45,6 +46,7 @@ public class Contour implements Releasable {
private Moments moments = null;
private MatOfPoint2f convexHull = null;
private MatOfPoint2f approxPolyDp = null;
public Contour(MatOfPoint mat) {
this.mat = mat;
@@ -68,6 +70,19 @@ public class Contour implements Releasable {
return convexHull;
}
public MatOfPoint2f getApproxPolyDp(double epsilon, boolean closed) {
if (this.approxPolyDp == null) {
approxPolyDp = new MatOfPoint2f();
Imgproc.approxPolyDP(getConvexHull(), approxPolyDp, epsilon, closed);
}
return approxPolyDp;
}
@Nullable
public MatOfPoint2f getApproxPolyDp() {
return this.approxPolyDp;
}
public double getArea() {
if (Double.isNaN(area)) {
area = Imgproc.contourArea(mat);
@@ -197,6 +212,7 @@ public class Contour implements Releasable {
if (mat != null) mat.release();
if (mat2f != null) mat2f.release();
if (convexHull != null) convexHull.release();
if (approxPolyDp != null) approxPolyDp.release();
}
public static MatOfPoint2f convertIndexesToPoints(MatOfPoint contour, MatOfInt indexes) {

View File

@@ -21,8 +21,8 @@ import java.util.EnumSet;
import java.util.HashMap;
public enum ContourShape {
Custom(-1),
Circle(0),
Custom(-1),
Triangle(3),
Quadrilateral(4);

View File

@@ -50,7 +50,7 @@ public class Collect2dTargetsPipe
params.frameStaticProperties);
for (PotentialTarget target : in) {
targets.add(new TrackedTarget(target, calculationParams));
targets.add(new TrackedTarget(target, calculationParams, target.shape));
}
return targets;

View File

@@ -27,6 +27,8 @@ import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.ColorHelper;
import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.pipe.MutatingPipe;
import org.photonvision.vision.target.TrackedTarget;
@@ -38,7 +40,9 @@ public class Draw2dTargetsPipe
@Override
protected Void process(Pair<Mat, List<TrackedTarget>> in) {
var imageSize = Math.sqrt(in.getLeft().rows() * in.getLeft().cols());
var imRows = in.getLeft().rows();
var imCols = in.getLeft().cols();
var imageSize = Math.sqrt(imRows * imCols);
var textSize = params.kPixelsToText * imageSize;
var thickness = params.kPixelsToThickness * imageSize;
@@ -53,6 +57,7 @@ public class Draw2dTargetsPipe
var centroidColour = ColorHelper.colorToScalar(params.centroidColor);
var maximumBoxColour = ColorHelper.colorToScalar(params.maximumBoxColor);
var rotatedBoxColour = ColorHelper.colorToScalar(params.rotatedBoxColor);
var circleColor = ColorHelper.colorToScalar(params.circleColor);
var shapeColour = ColorHelper.colorToScalar(params.shapeOutlineColour);
for (int i = 0; i < (params.showMultipleTargets ? in.getRight().size() : 1); i++) {
@@ -72,13 +77,35 @@ public class Draw2dTargetsPipe
dividePointArray(vertices);
contour.fromArray(vertices);
if (params.showRotatedBox) {
if (params.shouldShowRotatedBox(target.getShape())) {
Imgproc.drawContours(
in.getLeft(),
List.of(contour),
0,
rotatedBoxColour,
(int) Math.ceil(imageSize * params.kPixelsToBoxThickness));
} else if (params.shouldShowCircle(target.getShape())) {
Imgproc.circle(
in.getLeft(),
target.getShape().center,
(int) target.getShape().radius,
circleColor,
(int) Math.ceil(imageSize * params.kPixelsToBoxThickness));
} else {
// draw approximate polygon
var poly = target.getApproximateBoundingPolygon();
// fall back on the shape's approx poly dp
if (poly == null && target.getShape() != null)
poly = target.getShape().getContour().getApproxPolyDp();
if (poly != null) {
// divideMat2f(poly, pointMat);
var mat = new MatOfPoint();
mat.fromArray(poly.toArray());
Imgproc.drawContours(
in.getLeft(), List.of(mat), -1, ColorHelper.colorToScalar(Color.blue), 2);
mat.release();
}
}
if (params.showMaximumBox) {
@@ -193,12 +220,21 @@ public class Draw2dTargetsPipe
public Color maximumBoxColor = Color.RED;
public Color shapeOutlineColour = Color.MAGENTA;
public Color textColor = Color.GREEN;
public Color circleColor = Color.RED;
public final boolean showMultipleTargets;
public final boolean shouldDraw;
public final FrameDivisor divisor;
public boolean shouldShowRotatedBox(CVShape shape) {
return showRotatedBox && (shape == null || shape.shape.equals(ContourShape.Quadrilateral));
}
public boolean shouldShowCircle(CVShape shape) {
return shape != null && shape.shape.equals(ContourShape.Circle);
}
public Draw2dTargetsParams(
boolean shouldDraw, boolean showMultipleTargets, FrameDivisor divisor) {
this.shouldDraw = shouldDraw;

View File

@@ -17,13 +17,18 @@
package org.photonvision.vision.pipe.impl;
import java.util.ArrayList;
import java.util.List;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.pipe.CVPipe;
public class FilterShapesPipe
extends CVPipe<List<CVShape>, List<CVShape>, FilterShapesPipe.FilterShapesPipeParams> {
List<CVShape> outputList = new ArrayList<>();
/**
* Runs the process for the pipe.
*
@@ -32,30 +37,52 @@ public class FilterShapesPipe
*/
@Override
protected List<CVShape> process(List<CVShape> in) {
in.removeIf(
shape ->
shape.shape != params.desiredShape
|| shape.contour.getArea() > params.maxArea
|| shape.contour.getArea() < params.minArea
|| shape.contour.getPerimeter() > params.maxPeri
|| shape.contour.getPerimeter() < params.minPeri);
return in;
outputList.forEach(CVShape::release);
outputList.clear();
outputList = new ArrayList<>();
for (var shape : in) {
if (!shouldRemove(shape)) outputList.add(shape);
}
return outputList;
}
private boolean shouldRemove(CVShape shape) {
return shape.shape != params.desiredShape
|| shape.contour.getArea() / params.getFrameStaticProperties().imageArea * 100.0
> params.maxArea
|| shape.contour.getArea() / params.getFrameStaticProperties().imageArea * 100.0
< params.minArea
|| shape.contour.getPerimeter() > params.maxPeri
|| shape.contour.getPerimeter() < params.minPeri;
}
public static class FilterShapesPipeParams {
private final ContourShape desiredShape;
private final FrameStaticProperties frameStaticProperties;
private final double minArea;
private final double maxArea;
private final double minPeri;
private final double maxPeri;
public FilterShapesPipeParams(
ContourShape desiredShape, double minArea, double maxArea, double minPeri, double maxPeri) {
ContourShape desiredShape,
double minArea,
double maxArea,
double minPeri,
double maxPeri,
FrameStaticProperties frameStaticProperties) {
this.desiredShape = desiredShape;
this.minArea = minArea;
this.maxArea = maxArea;
this.minPeri = minPeri;
this.maxPeri = maxPeri;
this.frameStaticProperties = frameStaticProperties;
}
public FrameStaticProperties getFrameStaticProperties() {
return frameStaticProperties;
}
}
}

View File

@@ -21,11 +21,11 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.imgproc.Imgproc;
import org.opencv.imgproc.Moments;
import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.pipe.CVPipe;
public class FindCirclesPipe
@@ -49,6 +49,10 @@ public class FindCirclesPipe
circles.release();
List<CVShape> output = new ArrayList<>();
var diag = params.diagonalLengthPx;
var minRadius = (int) (params.minRadius * diag / 100.0);
var maxRadius = (int) (params.maxRadius * diag / 100.0);
Imgproc.HoughCircles(
in.getLeft(),
circles,
@@ -64,10 +68,15 @@ public class FindCirclesPipe
params.minDist,
params.maxCannyThresh,
params.accuracy,
params.minRadius,
params.maxRadius);
minRadius,
maxRadius);
// Great, we now found the center point of the circle and it's radius, but we have no idea what
// contour it corresponds to
// Each contour can only match to one circle, so we keep a list of unmatched contours around and
// only match against them
// This does mean that contours closer than allowableThreshold aren't matched to anything if
// there's a 'better' option
var unmatchedContours = in.getRight();
for (int x = 0; x < circles.cols(); x++) {
// Grab the current circle we are looking at
double[] c = circles.get(0, x);
@@ -75,17 +84,26 @@ public class FindCirclesPipe
double x_center = c[0];
double y_center = c[1];
for (Contour contour : in.getRight()) {
for (Contour contour : unmatchedContours) {
// Grab the moments of the current contour
Moments mu = contour.getMoments();
// Determine if the contour is within the threshold of the detected circle
// NOTE: This means that the centroid of the contour must be within the "allowable
// threshold"
// of the center of the circle
if (Math.abs(x_center - (mu.m10 / mu.m00)) <= params.allowableThreshold
&& Math.abs(y_center - (mu.m01 / mu.m00)) <= params.allowableThreshold) {
// If it is, then add it to the output array
output.add(new CVShape(contour, ContourShape.Circle));
output.add(new CVShape(contour, new Point(c[0], c[1]), c[2]));
unmatchedContours.remove(contour);
break;
}
}
}
// Release everything we don't use
for (var c : unmatchedContours) c.release();
return output;
}
@@ -96,6 +114,7 @@ public class FindCirclesPipe
private final int minDist;
private final int maxCannyThresh;
private final int accuracy;
private final double diagonalLengthPx;
/*
* @params minDist - Minimum distance between the centers of the detected circles.
@@ -114,13 +133,15 @@ public class FindCirclesPipe
int minDist,
int maxRadius,
int maxCannyThresh,
int accuracy) {
int accuracy,
double diagonalLengthPx) {
this.allowableThreshold = allowableThreshold;
this.minRadius = minRadius;
this.maxRadius = maxRadius;
this.minDist = minDist;
this.maxCannyThresh = maxCannyThresh;
this.accuracy = accuracy;
this.diagonalLengthPx = diagonalLengthPx;
}
}
}

View File

@@ -28,7 +28,7 @@ import org.photonvision.vision.pipe.CVPipe;
public class FindPolygonPipe
extends CVPipe<List<Contour>, List<CVShape>, FindPolygonPipe.FindPolygonPipeParams> {
private final MatOfPoint2f approx = new MatOfPoint2f();
List<CVShape> shapeList = new ArrayList<>();
/*
* Runs the process for the pipe.
@@ -38,18 +38,20 @@ public class FindPolygonPipe
*/
@Override
protected List<CVShape> process(List<Contour> in) {
// List containing all the output shapes
List<CVShape> output = new ArrayList<>();
shapeList.forEach(CVShape::release);
shapeList.clear();
shapeList = new ArrayList<>();
for (Contour contour : in) output.add(getShape(contour));
for (Contour contour : in) {
shapeList.add(getShape(contour));
}
return output;
return shapeList;
}
private CVShape getShape(Contour in) {
int corners = getCorners(in);
corners = getCorners(in);
/*The contourShape enum has predefined shapes for Circles, Triangles, and Quads
meaning any shape not fitting in those predefined shapes must be a custom shape.
@@ -70,14 +72,11 @@ public class FindPolygonPipe
}
private int getCorners(Contour contour) {
// Release previous approx
approx.release();
Imgproc.approxPolyDP(
contour.getMat2f(),
approx,
// Converts an accuracy percentage between 1-100 to an epsilon
params.accuracyPercentage / 600.0 * Imgproc.arcLength(contour.getMat2f(), true),
true);
var approx =
contour.getApproxPolyDp(
(100 - params.accuracyPercentage) / 100.0 * Imgproc.arcLength(contour.getMat2f(), true),
true);
// The height of the resultant approximation is the number of vertices
return (int) approx.size().height;
}

View File

@@ -45,6 +45,8 @@ public class SpeckleRejectPipe
for (Contour c : in) {
if (c.getArea() >= minAllowedArea) {
m_despeckledContours.add(c);
} else {
c.release();
}
}
}

View File

@@ -23,6 +23,9 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.raspi.PicamJNI;
import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.opencv.*;
import org.photonvision.vision.pipe.CVPipe.CVPipeResult;
@@ -38,13 +41,11 @@ public class ColoredShapePipeline
private final RotateImagePipe rotateImagePipe = new RotateImagePipe();
private final ErodeDilatePipe erodeDilatePipe = new ErodeDilatePipe();
private final HSVPipe hsvPipe = new HSVPipe();
private final OutputMatPipe outputMatPipe = new OutputMatPipe();
private final SpeckleRejectPipe speckleRejectPipe = new SpeckleRejectPipe();
private final FindContoursPipe findContoursPipe = new FindContoursPipe();
private final FindPolygonPipe findPolygonPipe = new FindPolygonPipe();
private final FindCirclesPipe findCirclesPipe = new FindCirclesPipe();
private final FilterShapesPipe filterShapesPipe = new FilterShapesPipe();
private final GroupContoursPipe groupContoursPipe = new GroupContoursPipe();
private final SortContoursPipe sortContoursPipe = new SortContoursPipe();
private final Collect2dTargetsPipe collect2dTargetsPipe = new Collect2dTargetsPipe();
private final CornerDetectionPipe cornerDetectionPipe = new CornerDetectionPipe();
@@ -54,7 +55,6 @@ public class ColoredShapePipeline
private final Draw3dTargetsPipe draw3dTargetsPipe = new Draw3dTargetsPipe();
private final CalculateFPSPipe calculateFPSPipe = new CalculateFPSPipe();
private final Mat rawInputMat = new Mat();
private final Point[] rectPoints = new Point[4];
public ColoredShapePipeline() {
@@ -79,6 +79,23 @@ public class ColoredShapePipeline
new RotateImagePipe.RotateImageParams(settings.inputImageRotationMode);
rotateImagePipe.setParams(rotateImageParams);
if (cameraQuirks.hasQuirk(CameraQuirk.PiCam) && PicamJNI.isSupported()) {
PicamJNI.setThresholds(
settings.hsvHue.getFirst() / 180d,
settings.hsvSaturation.getFirst() / 255d,
settings.hsvValue.getFirst() / 255d,
settings.hsvHue.getSecond() / 180d,
settings.hsvSaturation.getSecond() / 255d,
settings.hsvValue.getSecond() / 255d);
PicamJNI.setRotation(settings.inputImageRotationMode.value);
PicamJNI.setShouldCopyColor(settings.inputShouldShow);
} else {
var hsvParams =
new HSVPipe.HSVParams(settings.hsvHue, settings.hsvSaturation, settings.hsvValue);
hsvPipe.setParams(hsvParams);
}
ErodeDilatePipe.ErodeDilateParams erodeDilateParams =
new ErodeDilatePipe.ErodeDilateParams(settings.erode, settings.dilate, 5);
// TODO: add kernel size to pipeline settings
@@ -88,9 +105,6 @@ public class ColoredShapePipeline
new HSVPipe.HSVParams(settings.hsvHue, settings.hsvSaturation, settings.hsvValue);
hsvPipe.setParams(hsvParams);
OutputMatPipe.OutputMatParams outputMatParams = new OutputMatPipe.OutputMatParams();
outputMatPipe.setParams(outputMatParams);
SpeckleRejectPipe.SpeckleRejectParams speckleRejectParams =
new SpeckleRejectPipe.SpeckleRejectParams(settings.contourSpecklePercentage);
speckleRejectPipe.setParams(speckleRejectParams);
@@ -105,28 +119,25 @@ public class ColoredShapePipeline
FindCirclesPipe.FindCirclePipeParams findCirclePipeParams =
new FindCirclesPipe.FindCirclePipeParams(
settings.allowableThreshold,
settings.minRadius,
settings.circleDetectThreshold,
settings.contourRadius.getFirst(),
settings.minDist,
settings.maxRadius,
settings.contourRadius.getSecond(),
settings.maxCannyThresh,
settings.accuracy);
settings.circleAccuracy,
Math.hypot(frameStaticProperties.imageWidth, frameStaticProperties.imageHeight));
findCirclesPipe.setParams(findCirclePipeParams);
FilterShapesPipe.FilterShapesPipeParams filterShapesPipeParams =
new FilterShapesPipe.FilterShapesPipeParams(
settings.desiredShape,
settings.minArea,
settings.maxArea,
settings.minPeri,
settings.maxPeri);
settings.contourShape,
settings.contourArea.getFirst(),
settings.contourArea.getSecond(),
settings.contourPerimeter.getFirst(),
settings.contourPerimeter.getSecond(),
frameStaticProperties);
filterShapesPipe.setParams(filterShapesPipeParams);
GroupContoursPipe.GroupContoursParams groupContoursParams =
new GroupContoursPipe.GroupContoursParams(
settings.contourGroupingMode, settings.contourIntersection);
groupContoursPipe.setParams(groupContoursParams);
SortContoursPipe.SortContoursParams sortContoursParams =
new SortContoursPipe.SortContoursParams(
settings.contourSortMode,
@@ -193,17 +204,41 @@ public class ColoredShapePipeline
protected CVPipelineResult process(Frame frame, ColoredShapePipelineSettings settings) {
long sumPipeNanosElapsed = 0L;
var rotateImageResult = rotateImagePipe.run(frame.image.getMat());
sumPipeNanosElapsed += rotateImageResult.nanosElapsed;
CVPipeResult<Mat> hsvPipeResult;
Mat rawInputMat;
if (frame.image.getMat().channels() != 1) {
var rotateImageResult = rotateImagePipe.run(frame.image.getMat());
sumPipeNanosElapsed = rotateImageResult.nanosElapsed;
rawInputMat.release();
frame.image.getMat().copyTo(rawInputMat);
rawInputMat = frame.image.getMat();
var erodeDilateResult = erodeDilatePipe.run(rawInputMat);
sumPipeNanosElapsed += erodeDilateResult.nanosElapsed;
hsvPipeResult = hsvPipe.run(rawInputMat);
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
} else {
// Try to copy the color frame.
long inputMatPtr = PicamJNI.grabFrame(true);
if (inputMatPtr != 0) {
// If we grabbed it (in color copy mode), make a new Mat of it
rawInputMat = new Mat(inputMatPtr);
} else {
// Otherwise, use a blank/empty mat as placeholder
rawInputMat = new Mat();
}
CVPipeResult<Mat> hsvPipeResult = hsvPipe.run(rawInputMat);
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
// We can skip a few steps if the image is single channel because we've already done them on
// the GPU
hsvPipeResult = new CVPipeResult<>();
hsvPipeResult.output = frame.image.getMat();
hsvPipeResult.nanosElapsed = MathUtils.wpiNanoTime() - frame.timestampNanos;
sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
}
// var erodeDilateResult = erodeDilatePipe.run(rawInputMat);
// sumPipeNanosElapsed += erodeDilateResult.nanosElapsed;
//
// CVPipeResult<Mat> hsvPipeResult = hsvPipe.run(rawInputMat);
// sumPipeNanosElapsed += hsvPipeResult.nanosElapsed;
CVPipeResult<List<Contour>> findContoursResult = findContoursPipe.run(hsvPipeResult.output);
sumPipeNanosElapsed += findContoursResult.nanosElapsed;
@@ -212,8 +247,8 @@ public class ColoredShapePipeline
speckleRejectPipe.run(findContoursResult.output);
sumPipeNanosElapsed += speckleRejectResult.nanosElapsed;
List<CVShape> shapes;
if (settings.desiredShape == ContourShape.Circle) {
List<CVShape> shapes = null;
if (settings.contourShape == ContourShape.Circle) {
CVPipeResult<List<CVShape>> findCirclesResult =
findCirclesPipe.run(Pair.of(hsvPipeResult.output, speckleRejectResult.output));
sumPipeNanosElapsed += findCirclesResult.nanosElapsed;
@@ -228,15 +263,11 @@ public class ColoredShapePipeline
CVPipeResult<List<CVShape>> filterShapeResult = filterShapesPipe.run(shapes);
sumPipeNanosElapsed += filterShapeResult.nanosElapsed;
CVPipeResult<List<PotentialTarget>> groupContoursResult =
groupContoursPipe.run(
filterShapeResult.output.stream()
.map(CVShape::getContour)
.collect(Collectors.toList()));
sumPipeNanosElapsed += groupContoursResult.nanosElapsed;
CVPipeResult<List<PotentialTarget>> sortContoursResult =
sortContoursPipe.run(groupContoursResult.output);
sortContoursPipe.run(
filterShapeResult.output.stream()
.map(shape -> new PotentialTarget(shape.getContour(), shape))
.collect(Collectors.toList()));
sumPipeNanosElapsed += sortContoursResult.nanosElapsed;
CVPipeResult<List<TrackedTarget>> collect2dTargetsResult =
@@ -245,7 +276,7 @@ public class ColoredShapePipeline
List<TrackedTarget> targetList;
if (settings.solvePNPEnabled && settings.desiredShape == ContourShape.Circle) {
if (settings.solvePNPEnabled && settings.contourShape == ContourShape.Circle) {
var cornerDetectionResult = cornerDetectionPipe.run(collect2dTargetsResult.output);
collect2dTargetsResult.output.forEach(
shape -> {
@@ -262,37 +293,6 @@ public class ColoredShapePipeline
targetList = collect2dTargetsResult.output;
}
// Draw 2D Crosshair on input and output
var draw2dCrosshairResultOnInput = draw2dCrosshairPipe.run(Pair.of(rawInputMat, targetList));
sumPipeNanosElapsed += draw2dCrosshairResultOnInput.nanosElapsed;
var draw2dCrosshairResultOnOutput =
draw2dCrosshairPipe.run(Pair.of(hsvPipeResult.output, targetList));
sumPipeNanosElapsed += draw2dCrosshairResultOnOutput.nanosElapsed;
// Draw 2D contours on input and output
var draw2dContoursResultOnInput =
draw2DTargetsPipe.run(Pair.of(rawInputMat, collect2dTargetsResult.output));
sumPipeNanosElapsed += draw2dContoursResultOnInput.nanosElapsed;
var draw2dContoursResultOnOutput =
draw2DTargetsPipe.run(Pair.of(hsvPipeResult.output, collect2dTargetsResult.output));
sumPipeNanosElapsed += draw2dContoursResultOnOutput.nanosElapsed;
if (settings.solvePNPEnabled && settings.desiredShape == ContourShape.Circle) {
var drawOnInputResult =
draw3dTargetsPipe.run(Pair.of(rawInputMat, collect2dTargetsResult.output));
sumPipeNanosElapsed += drawOnInputResult.nanosElapsed;
var drawOnOutputResult =
draw3dTargetsPipe.run(Pair.of(hsvPipeResult.output, collect2dTargetsResult.output));
sumPipeNanosElapsed += drawOnOutputResult.nanosElapsed;
}
// Convert single-channel HSV output mat to 3-channel BGR in preparation for streaming
var outputMatPipeResult = outputMatPipe.run(hsvPipeResult.output);
sumPipeNanosElapsed += outputMatPipeResult.nanosElapsed;
var fpsResult = calculateFPSPipe.run(null);
var fps = fpsResult.output;

View File

@@ -19,6 +19,8 @@ package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.Objects;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.opencv.ContourGroupingMode;
import org.photonvision.vision.opencv.ContourIntersectionDirection;
@@ -27,19 +29,15 @@ import org.photonvision.vision.pipe.impl.CornerDetectionPipe;
@JsonTypeName("ColoredShapePipelineSettings")
public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
public ContourShape desiredShape = ContourShape.Triangle;
public double minArea = Integer.MIN_VALUE;
public double maxArea = Integer.MAX_VALUE;
public double minPeri = Integer.MIN_VALUE;
public double maxPeri = Integer.MAX_VALUE;
public ContourShape contourShape = ContourShape.Triangle;
public DoubleCouple contourPerimeter = new DoubleCouple(0, Double.MAX_VALUE);
public double accuracyPercentage = 10.0;
// Circle detection
public int allowableThreshold = 5;
public int minRadius = 0;
public int maxRadius = 0;
public int minDist = 10;
public int circleDetectThreshold = 5;
public IntegerCouple contourRadius = new IntegerCouple(0, 100);
public int minDist = 20;
public int maxCannyThresh = 90;
public int accuracy = 20;
public int circleAccuracy = 20;
// how many contours to attempt to group (Single, Dual)
public ContourGroupingMode contourGroupingMode = ContourGroupingMode.Single;
@@ -71,61 +69,50 @@ public class ColoredShapePipelineSettings extends AdvancedPipelineSettings {
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
ColoredShapePipelineSettings that = (ColoredShapePipelineSettings) o;
return Double.compare(that.minArea, minArea) == 0
&& Double.compare(that.maxArea, maxArea) == 0
&& Double.compare(that.minPeri, minPeri) == 0
&& Double.compare(that.maxPeri, maxPeri) == 0
&& Double.compare(that.accuracyPercentage, accuracyPercentage) == 0
&& allowableThreshold == that.allowableThreshold
&& minRadius == that.minRadius
&& maxRadius == that.maxRadius
return Double.compare(that.accuracyPercentage, accuracyPercentage) == 0
&& circleDetectThreshold == that.circleDetectThreshold
&& minDist == that.minDist
&& maxCannyThresh == that.maxCannyThresh
&& accuracy == that.accuracy
&& solvePNPEnabled == that.solvePNPEnabled
&& circleAccuracy == that.circleAccuracy
&& cornerDetectionUseConvexHulls == that.cornerDetectionUseConvexHulls
&& cornerDetectionExactSideCount == that.cornerDetectionExactSideCount
&& cornerDetectionSideCount == that.cornerDetectionSideCount
&& Double.compare(that.cornerDetectionAccuracyPercentage, cornerDetectionAccuracyPercentage)
== 0
&& desiredShape == that.desiredShape
&& erode == that.erode
&& dilate == that.dilate
&& contourShape == that.contourShape
&& Objects.equals(contourArea, that.contourArea)
&& Objects.equals(contourPerimeter, that.contourPerimeter)
&& Objects.equals(contourRadius, that.contourRadius)
&& contourGroupingMode == that.contourGroupingMode
&& contourIntersection == that.contourIntersection
&& Objects.equals(cameraCalibration, that.cameraCalibration)
&& Objects.equals(targetModel, that.targetModel)
&& cornerDetectionStrategy == that.cornerDetectionStrategy
&& erode == that.erode
&& dilate == that.dilate;
&& cornerDetectionStrategy == that.cornerDetectionStrategy;
}
@Override
public int hashCode() {
return Objects.hash(
super.hashCode(),
desiredShape,
minArea,
maxArea,
minPeri,
maxPeri,
contourShape,
contourArea,
contourPerimeter,
accuracyPercentage,
allowableThreshold,
minRadius,
maxRadius,
circleDetectThreshold,
contourRadius,
minDist,
maxCannyThresh,
accuracy,
circleAccuracy,
contourGroupingMode,
contourIntersection,
solvePNPEnabled,
cameraCalibration,
targetModel,
cornerDetectionStrategy,
cornerDetectionUseConvexHulls,
cornerDetectionExactSideCount,
cornerDetectionSideCount,
cornerDetectionAccuracyPercentage,
erode,
dilate,
accuracy);
dilate);
}
}

View File

@@ -20,11 +20,9 @@ package org.photonvision.vision.pipeline;
import com.fasterxml.jackson.annotation.JsonTypeName;
import org.photonvision.common.util.numbers.DoubleCouple;
import org.photonvision.vision.processes.PipelineManager;
import org.photonvision.vision.target.RobotOffsetPointMode;
@JsonTypeName("DriverModePipelineSettings")
public class DriverModePipelineSettings extends CVPipelineSettings {
public RobotOffsetPointMode offsetPointMode = RobotOffsetPointMode.None;
public DoubleCouple offsetPoint = new DoubleCouple();
public DriverModePipelineSettings() {

View File

@@ -22,6 +22,7 @@ import org.apache.commons.lang3.tuple.Pair;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.pipe.impl.*;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
@@ -108,7 +109,10 @@ public class OutputStreamPipeline {
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
// Draw 3D Targets on input and output if necessary
if (settings.solvePNPEnabled) {
if (settings.solvePNPEnabled
|| (settings.solvePNPEnabled
&& settings instanceof ColoredShapePipelineSettings
&& ((ColoredShapePipelineSettings) settings).contourShape == ContourShape.Circle)) {
var drawOnInputResult = draw3dTargetsPipe.run(Pair.of(inMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[7] = drawOnInputResult.nanosElapsed;

View File

@@ -22,7 +22,7 @@ public enum PipelineType {
Calib3d(-2, Calibrate3dPipeline.class),
DriverMode(-1, DriverModePipeline.class),
Reflective(0, ReflectivePipeline.class),
ColoredShape(0, ColoredShapePipeline.class);
ColoredShape(1, ColoredShapePipeline.class);
public final int baseIndex;
public final Class clazz;

View File

@@ -164,8 +164,8 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
// If we grabbed it (in color copy mode), make a new Mat of it
rawInputMat = new Mat(inputMatPtr);
} else {
// Otherwise, the input mat is frame we got from the camera
rawInputMat = frame.image.getMat();
// Otherwise, use a blank/empty mat as placeholder
rawInputMat = new Mat();
}
// We can skip a few steps if the image is single channel because we've already done them on

View File

@@ -18,6 +18,7 @@
package org.photonvision.vision.processes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.photonvision.common.configuration.CameraConfiguration;
@@ -243,23 +244,37 @@ public class PipelineManager {
}
public CVPipelineSettings addPipeline(PipelineType type, String nickname) {
var added = createSettingsForType(type, nickname);
if (added == null) {
logger.error("Cannot add null pipeline!");
return null;
}
addPipelineInternal(added);
reassignIndexes();
return added;
}
private CVPipelineSettings createSettingsForType(PipelineType type, String nickname) {
CVPipelineSettings newSettings;
switch (type) {
case Reflective:
{
var added = new ReflectivePipelineSettings();
added.pipelineNickname = nickname;
addPipelineInternal(added);
return added;
}
case ColoredShape:
{
var added = new ColoredShapePipelineSettings();
addPipelineInternal(added);
added.pipelineNickname = nickname;
return added;
}
default:
{
logger.error("Got invalid pipeline type: " + type.toString());
return null;
}
}
reassignIndexes();
return null;
}
private void addPipelineInternal(CVPipelineSettings settings) {
@@ -268,30 +283,41 @@ public class PipelineManager {
reassignIndexes();
}
private void removePipelineInternal(int index) {
/**
* Remove a pipeline settings at the given index and return the new current index
*
* @param index The idx to remove
*/
private int removePipelineInternal(int index) {
userPipelineSettings.remove(index);
currentPipelineIndex = Math.min(index, userPipelineSettings.size() - 1);
reassignIndexes();
return currentPipelineIndex;
}
public void setIndex(int index) {
this.setPipelineInternal(index);
}
public void removePipeline(int index) {
public int removePipeline(int index) {
if (index < 0) {
return;
return currentPipelineIndex;
}
// TODO should we block/lock on a mutex?
removePipelineInternal(index);
setIndex(currentPipelineIndex);
return removePipelineInternal(index);
}
public void renameCurrentPipeline(String newName) {
getCurrentPipelineSettings().pipelineNickname = newName;
}
public void duplicatePipeline(int index) {
/**
* Duplicate a pipeline at a given index
*
* @param index the index of the target pipeline
* @return The new index
*/
public int duplicatePipeline(int index) {
var settings = userPipelineSettings.get(index);
var newSettings = settings.clone();
newSettings.pipelineNickname =
@@ -300,24 +326,81 @@ public class PipelineManager {
logger.debug("Duplicating pipe " + index + " to " + newSettings.pipelineNickname);
userPipelineSettings.add(newSettings);
reassignIndexes();
// Now we look for the index of the new pipeline and return it
return userPipelineSettings.indexOf(newSettings);
}
private static String createUniqueName(
String nickname, List<CVPipelineSettings> existingSettings) {
int index = 0;
String uniqueName = nickname;
while (true) {
String finalUniqueName = uniqueName;
String finalUniqueName = uniqueName; // To get around lambda capture
var conflictingName =
existingSettings.stream().anyMatch(it -> it.pipelineNickname.equals(finalUniqueName));
if (!conflictingName) return uniqueName;
index++;
uniqueName = nickname + " (" + index + ")";
if (index == 6
&& existingSettings.stream()
.noneMatch(it -> it.pipelineNickname.equals(nickname + "( dQw4w9WgXcQ )")))
return nickname + "( dQw4w9WgXcQ )";
if (!conflictingName) {
// If no conflict, we're done
return uniqueName;
} else {
// Otherwise, we need to add a suffix to the name
// If the string doesn't already end in "([0-9]*)", we'll add it
// If it does, we'll increment the number in the suffix
if (uniqueName.matches(".*\\([0-9]*\\)")) {
// Because java strings are immutable, we have to do this curstedness
// This is like doing "New pipeline (" + 2 + ")"
var parenStart = uniqueName.lastIndexOf('(');
var parenEnd = uniqueName.length() - 1;
var number = Integer.parseInt(uniqueName.substring(parenStart + 1, parenEnd)) + 1;
uniqueName = uniqueName.substring(0, parenStart + 1) + number + ")";
} else {
uniqueName += " (1)";
}
}
}
}
public void changePipelineType(int newType) {
// Find the PipelineType proposed
// To do this we look at all the PipelineType entries and look for one with matching
// base indexes
PipelineType type =
Arrays.stream(PipelineType.values())
.filter(it -> it.baseIndex == newType)
.findAny()
.orElse(null);
if (type == null) {
logger.error("Could not match type " + newType + " to a PipelineType!");
return;
}
if (type.baseIndex == getCurrentPipelineSettings().pipelineType.baseIndex) {
logger.debug(
"Not changing settings as "
+ type
+ " and "
+ getCurrentPipelineSettings().pipelineType
+ " are identical!");
return;
}
// Our new settings will be totally nuked, but that's ok
// We *could* set things in common between the two, if we want
// But they're different enough it shouldn't be an issue
var name = getCurrentPipelineSettings().pipelineNickname;
var newSettings = createSettingsForType(type, name);
var idx = currentPipelineIndex;
if (idx < 0) {
logger.error("Cannot replace non-user pipeline!");
return;
}
logger.info("Adding new pipe of type " + type.toString() + " at idx " + idx);
userPipelineSettings.set(idx, newSettings);
setPipelineInternal(idx);
}
}

View File

@@ -257,11 +257,15 @@ public class VisionModule {
try {
var osr = outputStreamPipeline.process(inputFrame, outputFrame, settings, targets);
consumeFpsLimitedResult(osr);
} catch (Exception e) {
// Never die
logger.error("Exception while running stream runnable!", e);
}
try {
inputFrame.release();
outputFrame.release();
} catch (Exception e) {
// Never die
logger.error("Exception in stream runnable!", e);
logger.error("Exception freeing frames", e);
}
} else {
// busy wait! hurray!

View File

@@ -89,7 +89,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
case "deleteCurrPipeline":
var indexToDelete = parentModule.pipelineManager.getCurrentPipelineIndex();
logger.info("Deleting current pipe at index " + indexToDelete);
parentModule.pipelineManager.removePipeline(indexToDelete);
int newIndex = parentModule.pipelineManager.removePipeline(indexToDelete);
parentModule.setPipeline(newIndex);
parentModule.saveAndBroadcastAll();
return;
case "changePipeline": // change active pipeline
@@ -110,7 +111,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
parentModule.takeCalibrationSnapshot();
return;
case "duplicatePipeline":
parentModule.pipelineManager.duplicatePipeline((Integer) newPropValue);
int idx = parentModule.pipelineManager.duplicatePipeline((Integer) newPropValue);
parentModule.setPipeline(idx);
parentModule.saveAndBroadcastAll();
return;
case "robotOffsetPoint":
@@ -154,6 +156,10 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
}
}
return;
case "changePipelineType":
parentModule.pipelineManager.changePipelineType((Integer) newPropValue);
parentModule.saveAndBroadcastAll();
return;
}
// special case for camera settables
@@ -183,8 +189,8 @@ public class VisionModuleChangeSubscriber extends DataChangeSubscriber {
var actual = new DoubleCouple(orig.get(0), orig.get(1));
propField.set(currentSettings, actual);
} else if (propType.isAssignableFrom(IntegerCouple.class)) {
var orig = (ArrayList<Integer>) newPropValue;
var actual = new IntegerCouple(orig.get(0), orig.get(1));
var orig = (ArrayList<Number>) newPropValue;
var actual = new IntegerCouple(orig.get(0).intValue(), orig.get(1).intValue());
propField.set(currentSettings, actual);
} else if (propType.equals(Double.TYPE)) {
propField.setDouble(currentSettings, ((Number) newPropValue).doubleValue());

View File

@@ -22,6 +22,7 @@ import edu.wpi.cscore.UsbCameraInfo;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.dataflow.DataChangeService;
@@ -144,6 +145,11 @@ public class VisionSourceManager {
"After matching, "
+ unmatchedLoadedConfigs.size()
+ " configs remained unmatched. Is your camera disconnected?");
logger.warn(
"Unloaded configs: "
+ unmatchedLoadedConfigs.stream()
.map(it -> it.nickname)
.collect(Collectors.joining()));
hasWarned = true;
}
@@ -302,6 +308,7 @@ public class VisionSourceManager {
var cameraSources = new ArrayList<VisionSource>();
for (var configuration : camConfigs) {
if (configuration.baseName.startsWith("mmal service") && PicamJNI.isSupported()) {
configuration.cameraType = CameraType.ZeroCopyPicam;
var piCamSrc = new ZeroCopyPicamSource(configuration);
configuration.cameraType = CameraType.ZeroCopyPicam;

View File

@@ -20,6 +20,7 @@ package org.photonvision.vision.target;
import java.util.ArrayList;
import java.util.List;
import org.opencv.core.RotatedRect;
import org.photonvision.vision.opencv.CVShape;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.Releasable;
@@ -27,15 +28,24 @@ public class PotentialTarget implements Releasable {
public final Contour m_mainContour;
public final List<Contour> m_subContours;
public final CVShape shape;
public PotentialTarget(Contour inputContour) {
m_mainContour = inputContour;
m_subContours = new ArrayList<>(); // empty
this(inputContour, List.of());
}
public PotentialTarget(Contour inputContour, List<Contour> subContours) {
this(inputContour, subContours, null);
}
public PotentialTarget(Contour inputContour, List<Contour> subContours, CVShape shape) {
m_mainContour = inputContour;
m_subContours = new ArrayList<>(subContours);
this.shape = shape;
}
public PotentialTarget(Contour inputContour, CVShape shape) {
this(inputContour, List.of(), shape);
}
public RotatedRect getMinAreaRect() {
@@ -53,5 +63,6 @@ public class PotentialTarget implements Releasable {
sc.release();
}
m_subContours.clear();
if (shape != null) shape.release();
}
}

View File

@@ -25,9 +25,7 @@ import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.RotatedRect;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.opencv.Contour;
import org.photonvision.vision.opencv.DualOffsetValues;
import org.photonvision.vision.opencv.Releasable;
import org.photonvision.vision.opencv.*;
public class TrackedTarget implements Releasable {
public final Contour m_mainContour;
@@ -47,11 +45,15 @@ public class TrackedTarget implements Releasable {
private Transform2d m_cameraToTarget = new Transform2d();
private CVShape m_shape;
private Mat m_cameraRelativeTvec, m_cameraRelativeRvec;
public TrackedTarget(PotentialTarget origTarget, TargetCalculationParameters params) {
public TrackedTarget(
PotentialTarget origTarget, TargetCalculationParameters params, CVShape shape) {
this.m_mainContour = origTarget.m_mainContour;
this.m_subContours = origTarget.m_subContours;
this.m_shape = shape;
calculateValues(params);
}
@@ -170,13 +172,15 @@ public class TrackedTarget implements Releasable {
cameraRelativeRvec.copyTo(this.m_cameraRelativeRvec);
}
public CVShape getShape() {
return m_shape;
}
public void setShape(CVShape shape) {
this.m_shape = shape;
}
public HashMap<String, Object> toHashMap() {
// pitch: 0,
// yaw: 0,
// skew: 0,
// area: 0,
// // 3D only
// pose: {x: 0, y: 0, rot: 0},
var ret = new HashMap<String, Object>();
ret.put("pitch", getPitch());
ret.put("yaw", getYaw());

View File

@@ -68,8 +68,8 @@ public class ShapeBenchmarkTest {
pipeline.getSettings().outputShowMultipleTargets = true;
pipeline.getSettings().contourGroupingMode = ContourGroupingMode.Single;
pipeline.getSettings().contourIntersection = ContourIntersectionDirection.Up;
pipeline.getSettings().desiredShape = ContourShape.Custom;
pipeline.getSettings().allowableThreshold = 10;
pipeline.getSettings().contourShape = ContourShape.Custom;
pipeline.getSettings().circleDetectThreshold = 10;
pipeline.getSettings().accuracyPercentage = 30.0;
var frameProvider =
new FileFrameProvider(
@@ -89,8 +89,8 @@ public class ShapeBenchmarkTest {
pipeline.getSettings().outputShowMultipleTargets = true;
pipeline.getSettings().contourGroupingMode = ContourGroupingMode.Single;
pipeline.getSettings().contourIntersection = ContourIntersectionDirection.Up;
pipeline.getSettings().desiredShape = ContourShape.Custom;
pipeline.getSettings().allowableThreshold = 10;
pipeline.getSettings().contourShape = ContourShape.Custom;
pipeline.getSettings().circleDetectThreshold = 10;
pipeline.getSettings().accuracyPercentage = 30.0;
var frameProvider =
@@ -111,8 +111,8 @@ public class ShapeBenchmarkTest {
pipeline.getSettings().outputShowMultipleTargets = true;
pipeline.getSettings().contourGroupingMode = ContourGroupingMode.Single;
pipeline.getSettings().contourIntersection = ContourIntersectionDirection.Up;
pipeline.getSettings().desiredShape = ContourShape.Custom;
pipeline.getSettings().allowableThreshold = 10;
pipeline.getSettings().contourShape = ContourShape.Custom;
pipeline.getSettings().circleDetectThreshold = 10;
pipeline.getSettings().accuracyPercentage = 30.0;
var frameProvider =
@@ -133,8 +133,8 @@ public class ShapeBenchmarkTest {
pipeline.getSettings().outputShowMultipleTargets = true;
pipeline.getSettings().contourGroupingMode = ContourGroupingMode.Single;
pipeline.getSettings().contourIntersection = ContourIntersectionDirection.Up;
pipeline.getSettings().desiredShape = ContourShape.Custom;
pipeline.getSettings().allowableThreshold = 10;
pipeline.getSettings().contourShape = ContourShape.Custom;
pipeline.getSettings().circleDetectThreshold = 10;
pipeline.getSettings().accuracyPercentage = 30.0;
var frameProvider =

View File

@@ -79,6 +79,10 @@ public class Calibrate3dPipeTest {
assertTrue(calibrate3dPipeOutput.output.perViewErrors.length > 0);
System.out.println(
"Per View Errors: " + Arrays.toString(calibrate3dPipeOutput.output.perViewErrors));
for (var f : frames) {
f.release();
}
}
@Test
@@ -98,14 +102,14 @@ public class Calibrate3dPipeTest {
for (var file : directoryListing) {
calibration3dPipeline.takeSnapshot();
var output =
calibration3dPipeline.run(
new Frame(
new CVMat(Imgcodecs.imread(file.getAbsolutePath())),
new FrameStaticProperties(640, 480, 60, new Rotation2d(), null)),
QuirkyCamera.DefaultCamera);
var frame =
new Frame(
new CVMat(Imgcodecs.imread(file.getAbsolutePath())),
new FrameStaticProperties(640, 480, 60, new Rotation2d(), null));
var output = calibration3dPipeline.run(frame, QuirkyCamera.DefaultCamera);
// TestUtils.showImage(output.outputFrame.image.getMat());
output.release();
frame.release();
}
assertTrue(
@@ -114,13 +118,12 @@ public class Calibrate3dPipeTest {
.allMatch(it -> it.width() > 0 && it.height() > 0));
calibration3dPipeline.removeSnapshot(0);
calibration3dPipeline
.run(
new Frame(
new CVMat(Imgcodecs.imread(directoryListing[0].getAbsolutePath())),
new FrameStaticProperties(640, 480, 60, new Rotation2d(), null)),
QuirkyCamera.DefaultCamera)
.release();
var frame =
new Frame(
new CVMat(Imgcodecs.imread(directoryListing[0].getAbsolutePath())),
new FrameStaticProperties(640, 480, 60, new Rotation2d(), null));
calibration3dPipeline.run(frame, QuirkyCamera.DefaultCamera).release();
frame.release();
assertTrue(
calibration3dPipeline.foundCornersList.stream()
@@ -263,16 +266,16 @@ public class Calibrate3dPipeTest {
for (var file : directoryListing) {
if (file.isFile()) {
calibration3dPipeline.takeSnapshot();
var output =
calibration3dPipeline.run(
new Frame(
new CVMat(Imgcodecs.imread(file.getAbsolutePath())),
new FrameStaticProperties(
(int) imgRes.width, (int) imgRes.height, 67, new Rotation2d(), null)),
QuirkyCamera.DefaultCamera);
var frame =
new Frame(
new CVMat(Imgcodecs.imread(file.getAbsolutePath())),
new FrameStaticProperties(
(int) imgRes.width, (int) imgRes.height, 67, new Rotation2d(), null));
var output = calibration3dPipeline.run(frame, QuirkyCamera.DefaultCamera);
// TestUtils.showImage(output.outputFrame.image.getMat(), file.getName(), 1);
output.outputFrame.release();
output.release();
frame.release();
}
}

View File

@@ -58,7 +58,7 @@ public class CirclePNPTest {
}
private CameraCalibrationCoefficients getCoeffs(String filename) {
var cameraCalibration = TestUtils.getCoeffs(filename, true);
var cameraCalibration = TestUtils.getCoeffs(filename, false);
checkCameraCoefficients(cameraCalibration);
return cameraCalibration;
}
@@ -92,8 +92,8 @@ public class CirclePNPTest {
pipeline.getSettings().hsvValue.set(100, 255);
pipeline.getSettings().outputShouldDraw = true;
pipeline.getSettings().maxCannyThresh = 50;
pipeline.getSettings().accuracy = 15;
pipeline.getSettings().allowableThreshold = 5;
pipeline.getSettings().circleAccuracy = 15;
pipeline.getSettings().circleDetectThreshold = 5;
pipeline.getSettings().solvePNPEnabled = true;
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
@@ -103,9 +103,9 @@ public class CirclePNPTest {
pipeline.getSettings().outputShowMultipleTargets = false;
pipeline.getSettings().contourGroupingMode = ContourGroupingMode.Single;
pipeline.getSettings().contourIntersection = ContourIntersectionDirection.Up;
pipeline.getSettings().desiredShape = ContourShape.Circle;
pipeline.getSettings().allowableThreshold = 10;
pipeline.getSettings().minRadius = 30;
pipeline.getSettings().contourShape = ContourShape.Circle;
pipeline.getSettings().circleDetectThreshold = 10;
pipeline.getSettings().contourRadius.setFirst(30);
pipeline.getSettings().accuracyPercentage = 30.0;
var frameProvider =

View File

@@ -40,7 +40,7 @@ public class ColoredShapePipelineTest {
public static void testQuadrilateralDetection(
ColoredShapePipeline pipeline, ColoredShapePipelineSettings settings, Frame frame) {
settings.desiredShape = ContourShape.Quadrilateral;
settings.contourShape = ContourShape.Quadrilateral;
pipeline.settings = settings;
CVPipelineResult colouredShapePipelineResult = pipeline.run(frame, QuirkyCamera.DefaultCamera);
TestUtils.showImage(
@@ -50,7 +50,7 @@ public class ColoredShapePipelineTest {
public static void testCustomShapeDetection(
ColoredShapePipeline pipeline, ColoredShapePipelineSettings settings, Frame frame) {
settings.desiredShape = ContourShape.Custom;
settings.contourShape = ContourShape.Custom;
pipeline.settings = settings;
CVPipelineResult colouredShapePipelineResult = pipeline.run(frame, QuirkyCamera.DefaultCamera);
TestUtils.showImage(
@@ -61,7 +61,7 @@ public class ColoredShapePipelineTest {
@Test
public static void testCircleShapeDetection(
ColoredShapePipeline pipeline, ColoredShapePipelineSettings settings, Frame frame) {
settings.desiredShape = ContourShape.Circle;
settings.contourShape = ContourShape.Circle;
pipeline.settings = settings;
CVPipelineResult colouredShapePipelineResult = pipeline.run(frame, QuirkyCamera.DefaultCamera);
TestUtils.showImage(
@@ -77,8 +77,8 @@ public class ColoredShapePipelineTest {
settings.hsvSaturation.set(100, 255);
settings.hsvValue.set(100, 255);
settings.maxCannyThresh = 50;
settings.accuracy = 15;
settings.allowableThreshold = 5;
settings.circleAccuracy = 15;
settings.circleDetectThreshold = 5;
var frameProvider =
new FileFrameProvider(
TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_6, false),
@@ -102,16 +102,16 @@ public class ColoredShapePipelineTest {
settings.outputShowMultipleTargets = true;
settings.contourGroupingMode = ContourGroupingMode.Single;
settings.contourIntersection = ContourIntersectionDirection.Up;
settings.desiredShape = ContourShape.Triangle;
settings.allowableThreshold = 10;
settings.contourShape = ContourShape.Triangle;
settings.circleDetectThreshold = 10;
settings.accuracyPercentage = 30.0;
ColoredShapePipeline pipeline = new ColoredShapePipeline();
testTriangleDetection(pipeline, settings, frameProvider.get());
testQuadrilateralDetection(pipeline, settings, frameProvider.get());
testCustomShapeDetection(pipeline, settings, frameProvider.get());
testCircleShapeDetection(pipeline, settings, frameProvider.get());
testPowercellDetection(settings, pipeline);
// testCircleShapeDetection(pipeline, settings, frameProvider.get());
// testPowercellDetection(settings, pipeline);
}
private static void printTestResults(CVPipelineResult pipelineResult) {

View File

@@ -61,7 +61,7 @@ public class SolvePNPTest {
}
private CameraCalibrationCoefficients getCoeffs(String filename) {
var cameraCalibration = TestUtils.getCoeffs(filename, true);
var cameraCalibration = TestUtils.getCoeffs(filename, false);
checkCameraCoefficients(cameraCalibration);
return cameraCalibration;
}
@@ -106,7 +106,7 @@ public class SolvePNPTest {
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark48in, false),
TestUtils.WPI2019Image.FOV,
new Rotation2d(),
TestUtils.get2019LifeCamCoeffs(true));
TestUtils.get2019LifeCamCoeffs(false));
CVPipelineResult pipelineResult;
@@ -141,7 +141,7 @@ public class SolvePNPTest {
TestUtils.getWPIImagePath(TestUtils.WPI2020Image.kBlueGoal_224in_Left, false),
TestUtils.WPI2020Image.FOV,
new Rotation2d(),
TestUtils.get2020LifeCamCoeffs(true));
TestUtils.get2020LifeCamCoeffs(false));
CVPipelineResult pipelineResult = pipeline.run(frameProvider.get(), QuirkyCamera.DefaultCamera);
printTestResults(pipelineResult);

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.vision.processes;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
import org.photonvision.vision.pipeline.PipelineType;
public class PipelineManagerTest {
@Test
public void testUniqueName() {
TestUtils.loadLibraries();
PipelineManager manager = new PipelineManager(new DriverModePipelineSettings(), List.of());
manager.addPipeline(PipelineType.Reflective, "Another");
// We now have ["New Pipeline", "Another"]
// After we duplicate 0 and 1, we expect ["New Pipeline", "Another", "New Pipeline (1)",
// "Another (1)"]
manager.duplicatePipeline(0);
manager.duplicatePipeline(1);
// Should add "Another (2)"
manager.duplicatePipeline(3);
// Should add "Another (3)
manager.duplicatePipeline(3);
// Should add "Another (4)
manager.duplicatePipeline(1);
// Should add "Another (5)" through "Another (15)"
for (int i = 5; i < 15; i++) {
manager.duplicatePipeline(1);
}
var nicks = manager.getPipelineNicknames();
var expected =
new ArrayList<>(List.of("New Pipeline", "Another", "New Pipeline (1)", "Another (1)"));
for (int i = 2; i < 15; i++) {
expected.add("Another (" + i + ")");
}
Assertions.assertEquals(expected, nicks);
}
}

View File

@@ -45,6 +45,7 @@ public class TrackedTargetTest {
new Point(426.22, 302),
new Point(400, 302))); // gives contour with center of 426, 300
Contour contour = new Contour(mat);
var pTarget = new PotentialTarget(contour);
var imageSize = new Size(800, 600);
@@ -61,7 +62,7 @@ public class TrackedTargetTest {
34.3,
imageSize.area());
var trackedTarget = new TrackedTarget(pTarget, setting);
var trackedTarget = new TrackedTarget(pTarget, setting, null);
// TODO change these hardcoded values
assertEquals(12.0, trackedTarget.getYaw(), 0.05, "Yaw was incorrect");
assertEquals(0, trackedTarget.getPitch(), 0.05, "Pitch was incorrect");

View File

@@ -15,6 +15,7 @@ includeProject {
includeOtherLibs {
^frc/
^networktables/
^units/
^wpi/
}

View File

@@ -4,13 +4,14 @@
"version": "${photon_version}",
"uuid": "515fe07e-bfc6-11fa-b3de-0242ac130004 ",
"mavenUrls": [
"https://maven.photonvision.org/repository/internal"
"https://maven.photonvision.org/repository/internal",
"https://maven.photonvision.org/repository/snapshots"
],
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/lib/PhotonLib-json/1.0/PhotonLib-json-1.0.json",
"jsonUrl": "https://maven.photonvision.org/repository/internal/org/photonvision/PhotonLib-json/1.0/PhotonLib-json-1.0.json",
"jniDependencies": [],
"cppDependencies": [
{
"groupId": "org.photonvision.lib",
"groupId": "org.photonvision",
"artifactId": "PhotonLib-cpp",
"version": "${photon_version}",
"libName": "Photon",

View File

@@ -195,6 +195,7 @@ public class PhotonCamera {
* @deprecated This method should be replaced with {@link PhotonPipelineResult#hasTargets()}
* @return Whether the latest target result has targets.
*/
@Deprecated
public boolean hasTargets() {
return getLatestResult().hasTargets();
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision;
import java.util.Comparator;
import org.photonvision.targeting.PhotonTrackedTarget;
public enum PhotonTargetSortMode {
Smallest(Comparator.comparingDouble(PhotonTrackedTarget::getArea)),
Largest(Smallest.m_comparator.reversed()),
Highest(Comparator.comparingDouble(PhotonTrackedTarget::getPitch)),
Lowest(Highest.m_comparator.reversed()),
Rightmost(Comparator.comparingDouble(PhotonTrackedTarget::getYaw)),
Leftmost(Rightmost.m_comparator.reversed()),
Centermost(
Comparator.comparingDouble(
target -> (Math.pow(target.getPitch(), 2) + Math.pow(target.getYaw(), 2))));
private final Comparator<PhotonTrackedTarget> m_comparator;
PhotonTargetSortMode(Comparator<PhotonTrackedTarget> comparator) {
m_comparator = comparator;
}
public Comparator<PhotonTrackedTarget> getComparator() {
return m_comparator;
}
}

View File

@@ -18,13 +18,24 @@
package org.photonvision;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEntry;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.Arrays;
import java.util.List;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.targeting.PhotonPipelineResult;
import org.photonvision.targeting.PhotonTrackedTarget;
@SuppressWarnings("unused")
public class SimPhotonCamera extends PhotonCamera {
private final NetworkTableEntry latencyMillisEntry;
private final NetworkTableEntry hasTargetEntry;
private final NetworkTableEntry targetPitchEntry;
private final NetworkTableEntry targetYawEntry;
private final NetworkTableEntry targetAreaEntry;
private final NetworkTableEntry targetSkewEntry;
private final NetworkTableEntry targetPoseEntry;
/**
* Constructs a Simulated PhotonCamera from a root table.
*
@@ -32,6 +43,14 @@ public class SimPhotonCamera extends PhotonCamera {
*/
public SimPhotonCamera(NetworkTable rootTable) {
super(rootTable);
latencyMillisEntry = rootTable.getEntry("latencyMillis");
hasTargetEntry = rootTable.getEntry("hasTargetEntry");
targetPitchEntry = rootTable.getEntry("targetPitchEntry");
targetYawEntry = rootTable.getEntry("targetYawEntry");
targetAreaEntry = rootTable.getEntry("targetAreaEntry");
targetSkewEntry = rootTable.getEntry("targetSkewEntry");
targetPoseEntry = rootTable.getEntry("targetPoseEntry");
}
/**
@@ -40,13 +59,13 @@ public class SimPhotonCamera extends PhotonCamera {
* @param cameraName The nickname of the camera (found in the PhotonVision UI).
*/
public SimPhotonCamera(String cameraName) {
super(cameraName);
this(NetworkTableInstance.getDefault().getTable("photonvision").getSubTable(cameraName));
}
/**
* Simulate one processed frame of vision data, putting one result to NT.
*
* @param latencyMillis
* @param latencyMillis Latency of the provided frame
* @param targets Each target detected
*/
public void submitProcessedFrame(double latencyMillis, PhotonTrackedTarget... targets) {
@@ -56,15 +75,66 @@ public class SimPhotonCamera extends PhotonCamera {
/**
* Simulate one processed frame of vision data, putting one result to NT.
*
* @param latencyMillis
* @param tgtList List of targets detected
* @param latencyMillis Latency of the provided frame
* @param sortMode Order in which to sort targets
* @param targets Each target detected
*/
public void submitProcessedFrame(double latencyMillis, List<PhotonTrackedTarget> tgtList) {
if (!getDriverMode()) {
PhotonPipelineResult newResult = new PhotonPipelineResult(latencyMillis, tgtList);
var newPacket = new Packet(newResult.getPacketSize());
newResult.populatePacket(newPacket);
rawBytesEntry.setRaw(newPacket.getData());
public void submitProcessedFrame(
double latencyMillis, PhotonTargetSortMode sortMode, PhotonTrackedTarget... targets) {
submitProcessedFrame(latencyMillis, sortMode, Arrays.asList(targets));
}
/**
* Simulate one processed frame of vision data, putting one result to NT.
*
* @param latencyMillis Latency of the provided frame
* @param targetList List of targets detected
*/
public void submitProcessedFrame(double latencyMillis, List<PhotonTrackedTarget> targetList) {
submitProcessedFrame(latencyMillis, null, targetList);
}
/**
* Simulate one processed frame of vision data, putting one result to NT.
*
* @param latencyMillis Latency of the provided frame
* @param sortMode Order in which to sort targets
* @param targetList List of targets detected
*/
public void submitProcessedFrame(
double latencyMillis, PhotonTargetSortMode sortMode, List<PhotonTrackedTarget> targetList) {
latencyMillisEntry.setDouble(latencyMillis);
if (sortMode != null) {
targetList.sort(sortMode.getComparator());
}
PhotonPipelineResult newResult = new PhotonPipelineResult(latencyMillis, targetList);
var newPacket = new Packet(newResult.getPacketSize());
newResult.populatePacket(newPacket);
rawBytesEntry.setRaw(newPacket.getData());
boolean hasTargets = newResult.hasTargets();
hasTargetEntry.setBoolean(hasTargets);
if (!hasTargets) {
targetPitchEntry.setDouble(0.0);
targetYawEntry.setDouble(0.0);
targetAreaEntry.setDouble(0.0);
targetPoseEntry.setDoubleArray(new double[] {0.0, 0.0, 0.0});
targetSkewEntry.setDouble(0.0);
} else {
var bestTarget = newResult.getBestTarget();
targetPitchEntry.setDouble(bestTarget.getPitch());
targetYawEntry.setDouble(bestTarget.getYaw());
targetAreaEntry.setDouble(bestTarget.getArea());
targetSkewEntry.setDouble(bestTarget.getSkew());
var transform = bestTarget.getCameraToTarget();
double[] poseData = {
transform.getX(), transform.getY(), transform.getRotation().getDegrees()
};
targetPoseEntry.setDoubleArray(poseData);
}
}
}

View File

@@ -41,10 +41,10 @@ public class SimVisionSystem {
/**
* Create a simulated vision system involving a camera and coprocessor mounted on a mobile robot
* running Photonvision, detecting one or more targets scattered around the field. This assumes a
* fairly simple and distortionless pinhole camera model.
* running PhotonVision, detecting one or more targets scattered around the field. This assumes a
* fairly simple and distortion-less pinhole camera model.
*
* @param camName Name of the photonvision camera to create. Align it with the settings you use in
* @param camName Name of the PhotonVision camera to create. Align it with the settings you use in
* the PhotonVision GUI.
* @param camDiagFOVDegrees Diagonal Field of View of the camera used. Align it with the
* manufacturer specifications, and/or whatever is configured in the PhotonVision Setting
@@ -87,25 +87,25 @@ public class SimVisionSystem {
this.camVertFOVDegrees = camDiagFOVDegrees * cameraResHeight / hypotPixels;
cam = new SimPhotonCamera(camName);
tgtList = new ArrayList<SimVisionTarget>();
tgtList = new ArrayList<>();
}
/**
* Add a target on the field which your vision system is designed to detect. The photoncamera from
* this system will report the location of the robot relative to the subste of these targets which
* Add a target on the field which your vision system is designed to detect. The PhotonCamera from
* this system will report the location of the robot relative to the subset of these targets which
* are visible from the given robot position.
*
* @param tgt
* @param target Target to add to the simulated field
*/
public void addSimVisionTarget(SimVisionTarget tgt) {
tgtList.add(tgt);
public void addSimVisionTarget(SimVisionTarget target) {
tgtList.add(target);
}
/**
* Adjust the camera position relative to the robot. Use this if your camera is on a gimbal or
* turret or some other mobile platform.
*
* @param newCameraToRobot New Tranform from the robot to the camera
* @param newCameraToRobot New Transform from the robot to the camera
* @param newCamHeightMeters New height of the camera off the floor
* @param newCamPitchDegrees New pitch of the camera axis back from horizontal
*/
@@ -120,12 +120,11 @@ public class SimVisionSystem {
* Periodic update. Call this once per frame of image data you wish to process and send to
* NetworkTables
*
* @param robotPoseMeters current pose of the robot on the field. Will be used to calcualte which
* @param robotPoseMeters current pose of the robot on the field. Will be used to calculate which
* targets are actually in view, where they are at relative to the robot, and relevant
* PhotonVision parameters.
*/
public void processFrame(Pose2d robotPoseMeters) {
Pose2d cameraPos = robotPoseMeters.transformBy(cameraToRobot.inverse());
ArrayList<PhotonTrackedTarget> visibleTgtList = new ArrayList<>(tgtList.size());

View File

@@ -24,7 +24,6 @@ public class SimVisionTarget {
double targetWidthMeters;
double targetHeightMeters;
double targetHeightAboveGroundMeters;
double targetInfill_pct;
double tgtAreaMeters2;
/**
@@ -34,8 +33,8 @@ public class SimVisionTarget {
* the middle of the field facing the target, the Y axis points to your left, and the X axis
* points away from you.
* @param targetHeightAboveGroundMeters Height of the target above the field plane, in meters.
* @param targetWidthMeters Width of the outter bounding box of the target in meters.
* @param targetHeightMeters Pair Height of the outter bounding box of the target in meters.
* @param targetWidthMeters Width of the outer bounding box of the target in meters.
* @param targetHeightMeters Pair Height of the outer bounding box of the target in meters.
*/
public SimVisionTarget(
Pose2d targetPos,

View File

@@ -45,7 +45,10 @@ PhotonPipelineResult PhotonCamera::GetLatestResult() const {
PhotonPipelineResult result;
// Fill the packet with latest data and populate result.
std::string value = rawBytesEntry.GetValue()->GetRaw();
std::shared_ptr<nt::Value> ntvalue = rawBytesEntry.GetValue();
if (!ntvalue) return result;
std::string value = ntvalue->GetRaw();
std::vector<char> bytes{value.begin(), value.end()};
photonlib::Packet packet{bytes};

View File

@@ -21,13 +21,10 @@ namespace photonlib {
PhotonPipelineResult::PhotonPipelineResult(
units::second_t latency, wpi::ArrayRef<PhotonTrackedTarget> targets)
: latency(latency),
targets(targets.data(), targets.data() + targets.size()) {
hasTargets = targets.size() != 0;
}
targets(targets.data(), targets.data() + targets.size()) {}
bool PhotonPipelineResult::operator==(const PhotonPipelineResult& other) const {
return latency == other.latency && hasTargets == other.hasTargets &&
targets == other.targets;
return latency == other.latency && targets == other.targets;
}
bool PhotonPipelineResult::operator!=(const PhotonPipelineResult& other) const {
@@ -35,8 +32,8 @@ bool PhotonPipelineResult::operator!=(const PhotonPipelineResult& other) const {
}
Packet& operator<<(Packet& packet, const PhotonPipelineResult& result) {
// Encode latency, existence of targets, and number of targets.
packet << result.latency.to<double>() * 1000 << result.hasTargets
// Encode latency and number of targets.
packet << result.latency.to<double>() * 1000
<< static_cast<int8_t>(result.targets.size());
// Encode the information of each target.
@@ -50,7 +47,7 @@ Packet& operator>>(Packet& packet, PhotonPipelineResult& result) {
// Decode latency, existence of targets, and number of targets.
int8_t targetCount = 0;
double latencyMillis = 0;
packet >> latencyMillis >> result.hasTargets >> targetCount;
packet >> latencyMillis >> targetCount;
result.latency = units::second_t(latencyMillis / 1000.0);
result.targets.clear();

View File

@@ -17,12 +17,13 @@
#pragma once
#include <memory>
#include <string>
#include <networktables/NetworkTable.h>
#include <networktables/NetworkTableEntry.h>
#include <networktables/NetworkTableInstance.h>
#include <memory>
#include <string>
#include <wpi/deprecated.h>
#include "photonlib/PhotonPipelineResult.h"
@@ -118,9 +119,11 @@ class PhotonCamera {
* This method is deprecated; {@link PhotonPipelineResult#hasTargets()} should
* be used instead.
* @deprecated This method should be replaced with {@link
* PhotonPipelineResult#hasTargets()}
* PhotonPipelineResult#HasTargets()}
* @return Whether the latest target result has targets.
*/
WPI_DEPRECATED(
"This method should be replaced with PhotonPipelineResult::HasTargets()")
bool HasTargets() const { return GetLatestResult().HasTargets(); }
private:

View File

@@ -57,12 +57,12 @@ class PhotonPipelineResult {
if (!HasTargets() && !HAS_WARNED) {
::frc::DriverStation::ReportError(
"This PhotonPipelineResult object has no targets associated with it! "
"Please check hasTargets() before calling this method. For more "
"Please check HasTargets() before calling this method. For more "
"information, please review the PhotonLib documentation at "
"http://docs.photonvision.org");
HAS_WARNED = true;
}
return hasTargets ? targets[0] : PhotonTrackedTarget();
return HasTargets() ? targets[0] : PhotonTrackedTarget();
}
/**
@@ -75,7 +75,7 @@ class PhotonPipelineResult {
* Returns whether the pipeline has targets.
* @return Whether the pipeline has targets.
*/
bool HasTargets() const { return hasTargets; }
bool HasTargets() const { return targets.size() > 0; }
/**
* Returns a reference to the vector of targets.
@@ -92,8 +92,7 @@ class PhotonPipelineResult {
friend Packet& operator>>(Packet& packet, PhotonPipelineResult& result);
private:
units::second_t latency;
bool hasTargets;
units::second_t latency = 0_s;
wpi::SmallVector<PhotonTrackedTarget, 10> targets;
inline static bool HAS_WARNED = false;
};

View File

@@ -38,7 +38,6 @@ class SimVisionTarget {
units::meter_t targetHeightAboveGround;
units::meter_t targetWidth;
units::meter_t targetHeight;
double targetInfill_pct;
units::square_meter_t tgtArea;
};

View File

@@ -49,7 +49,6 @@ class PhotonUtilTest {
@Test
public void testTransform() {
var camHeight = 1;
var tgtHeight = 3;
var camPitch = 0;

View File

@@ -53,7 +53,6 @@ class SimVisionSystemTest {
@ParameterizedTest
@ValueSource(doubles = {5, 10, 15, 20, 25, 30})
public void testDistanceAligned(double dist) {
final var targetPose = new Pose2d(new Translation2d(35, 0), new Rotation2d());
var sysUnderTest =
new SimVisionSystem("Test", 80.0, 0.0, new Transform2d(), 1, 99999, 320, 240, 0);

View File

@@ -18,4 +18,4 @@
#include "gtest/gtest.h"
#include "photonlib/PhotonUtils.h"
TEST(PhotonUtilsTest, TestInclude) {}
TEST(PhotonUtilsTest, Include) {}

View File

@@ -18,7 +18,6 @@
#include <networktables/NetworkTable.h>
#include <networktables/NetworkTableEntry.h>
#include <networktables/NetworkTableInstance.h>
#include <units/angle.h>
#include <units/length.h>
@@ -27,7 +26,7 @@
#include "photonlib/PhotonUtils.h"
#include "photonlib/SimVisionSystem.h"
TEST(SimVisionSystemTest, testEmpty) {
TEST(SimVisionSystemTest, Empty) {
photonlib::SimVisionSystem sysUnderTest("Test", 80.0_deg, 0.0_deg,
frc::Transform2d(), 1.0_m, 99999.0_m,
320, 240, 0.0);
@@ -37,12 +36,12 @@ TEST(SimVisionSystemTest, testEmpty) {
}
}
class SimVisionSystemTestDistParam : public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemTestDistParamInst,
SimVisionSystemTestDistParam,
class SimVisionSystemDistParamTest : public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemDistParamTests,
SimVisionSystemDistParamTest,
testing::Values(5, 10, 15, 20, 25, 30));
TEST_P(SimVisionSystemTestDistParam, testDistanceAligned) {
TEST_P(SimVisionSystemDistParamTest, DistanceAligned) {
double dist = GetParam();
auto targetPose =
@@ -69,7 +68,7 @@ TEST_P(SimVisionSystemTestDistParam, testDistanceAligned) {
dist);
}
TEST(SimVisionSystemTest, testVisibilityCupidShuffle) {
TEST(SimVisionSystemTest, VisibilityCupidShuffle) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
@@ -138,7 +137,7 @@ TEST(SimVisionSystemTest, testVisibilityCupidShuffle) {
EXPECT_TRUE(result.HasTargets());
}
TEST(SimVisionSystemTest, testNotVisibleVert1) {
TEST(SimVisionSystemTest, NotVisibleVert1) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
@@ -164,7 +163,7 @@ TEST(SimVisionSystemTest, testNotVisibleVert1) {
EXPECT_FALSE(result.HasTargets());
}
TEST(SimVisionSystemTest, testNotVisibleVert2) {
TEST(SimVisionSystemTest, NotVisibleVert2) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
@@ -189,7 +188,7 @@ TEST(SimVisionSystemTest, testNotVisibleVert2) {
EXPECT_FALSE(result.HasTargets());
}
TEST(SimVisionSystemTest, testNotVisibleTgtSize) {
TEST(SimVisionSystemTest, NotVisibleTgtSize) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
@@ -213,7 +212,7 @@ TEST(SimVisionSystemTest, testNotVisibleTgtSize) {
EXPECT_FALSE(result.HasTargets());
}
TEST(SimVisionSystemTest, testNotVisibleTooFarForLEDs) {
TEST(SimVisionSystemTest, NotVisibleTooFarForLEDs) {
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d());
@@ -237,11 +236,11 @@ TEST(SimVisionSystemTest, testNotVisibleTooFarForLEDs) {
EXPECT_FALSE(result.HasTargets());
}
class SimVisionSystemTestYawParam : public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemTestYawParamInst,
SimVisionSystemTestYawParam,
class SimVisionSystemYawParamTest : public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemYawParamTests,
SimVisionSystemYawParamTest,
testing::Values(-10, -5, -0, -1, -2, 5, 7, 10.23));
TEST_P(SimVisionSystemTestYawParam, testYawAngles) {
TEST_P(SimVisionSystemYawParamTest, YawAngles) {
double testYaw = GetParam(); // Nope, Chuck testYaw
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d(45_deg));
@@ -263,13 +262,13 @@ TEST_P(SimVisionSystemTestYawParam, testYawAngles) {
EXPECT_DOUBLE_EQ(tgt.GetYaw(), testYaw);
}
class SimVisionSystemTestCameraPitchParam
class SimVisionSystemCameraPitchParamTest
: public testing::TestWithParam<double> {};
INSTANTIATE_TEST_SUITE_P(SimVisionSystemTestCameraPitchParamInst,
SimVisionSystemTestCameraPitchParam,
INSTANTIATE_TEST_SUITE_P(SimVisionSystemCameraPitchParamTests,
SimVisionSystemCameraPitchParamTest,
testing::Values(-10, -5, -0, -1, -2, 5, 7, 10.23,
20.21, -19.999));
TEST_P(SimVisionSystemTestCameraPitchParam, testCameraPitch) {
TEST_P(SimVisionSystemCameraPitchParamTest, CameraPitch) {
double testPitch = GetParam();
auto targetPose =
frc::Pose2d(frc::Translation2d(35_m, 0_m), frc::Rotation2d(45_deg));
@@ -298,10 +297,10 @@ TEST_P(SimVisionSystemTestCameraPitchParam, testCameraPitch) {
EXPECT_DOUBLE_EQ(tgt.GetPitch(), -1.0 * testPitch);
}
class SimVisionSystemTestDistCalcParam
class SimVisionSystemDistCalcParamTest
: public testing::TestWithParam<std::tuple<double, double, double>> {};
INSTANTIATE_TEST_SUITE_P(
SimVisionSystemTestDistCalcParamInst, SimVisionSystemTestDistCalcParam,
SimVisionSystemDistCalcParamTests, SimVisionSystemDistCalcParamTest,
testing::Values(std::tuple<double, double, double>(5, 35, 0),
std::tuple<double, double, double>(6, 35, 1),
std::tuple<double, double, double>(10, 35, 0),
@@ -321,7 +320,7 @@ INSTANTIATE_TEST_SUITE_P(
std::tuple<double, double, double>(19.52, 35, 1.1),
std::tuple<double, double, double>(20, 51, 2.87),
std::tuple<double, double, double>(20, 55, 3)));
TEST_P(SimVisionSystemTestDistCalcParam, testDistanceCalc) {
TEST_P(SimVisionSystemDistCalcParamTest, DistanceCalc) {
std::tuple<double, double, double> testArgs = GetParam();
double testDist = std::get<0>(testArgs);
double testPitch = std::get<1>(testArgs);
@@ -354,7 +353,7 @@ TEST_P(SimVisionSystemTestDistCalcParam, testDistanceCalc) {
EXPECT_DOUBLE_EQ(distMeas.to<double>(), testDist);
}
TEST(SimVisionSystemTest, testMultipleTargets) {
TEST(SimVisionSystemTest, MultipleTargets) {
auto targetPoseL =
frc::Pose2d(frc::Translation2d(35_m, 2_m), frc::Rotation2d());
auto targetPoseC =

View File

@@ -30,12 +30,15 @@ import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.util.TestUtils;
import org.photonvision.common.util.numbers.IntegerCouple;
import org.photonvision.raspi.PicamJNI;
import org.photonvision.server.Server;
import org.photonvision.vision.camera.FileVisionSource;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.ContourGroupingMode;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.pipeline.CVPipelineSettings;
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
import org.photonvision.vision.pipeline.PipelineProfiler;
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
import org.photonvision.vision.processes.VisionModule;
@@ -57,13 +60,11 @@ public class Main {
final var options = new Options();
options.addOption("d", "debug", false, "Enable debug logging prints");
options.addOption("h", "help", false, "Show this help text and exit");
if (!isRelease) {
options.addOption(
"t",
"test-mode",
false,
"Run in test mode with 2019 and 2020 WPI field images in place of cameras");
}
options.addOption(
"t",
"test-mode",
false,
"Run in test mode with 2019 and 2020 WPI field images in place of cameras");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
@@ -125,6 +126,23 @@ public class Main {
collectedSources.add(fvs2019);
collectedSources.add(fvs2020);
// Colored shape testing
var camConfShape =
new CameraConfiguration(
"Shape",
TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_1, true)
.toString());
var settings = new ColoredShapePipelineSettings();
settings.hsvHue = new IntegerCouple(0, 35);
settings.hsvSaturation = new IntegerCouple(82, 255);
settings.hsvValue = new IntegerCouple(62, 255);
settings.contourShape = ContourShape.Triangle;
settings.outputShowMultipleTargets = true;
settings.circleAccuracy = 15;
camConfShape.addPipelineSetting(settings);
var fvsShape = new FileVisionSource(camConfShape);
collectedSources.add(fvsShape);
// logger.info("Adding " + allSources.size() + " configs to VMM.");
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
ConfigManager.getInstance().addCameraConfigurations(collectedSources);

View File

@@ -26,6 +26,7 @@ import io.javalin.websocket.WsConnectContext;
import io.javalin.websocket.WsContext;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -183,8 +184,9 @@ public class SocketHandler {
// var type = (PipelineType)
// data.get("pipelineType");
// var name = (String) data.get("pipelineName");
var type = PipelineType.Reflective;
var name = (String) entryValue;
var arr = (ArrayList<Object>) entryValue;
var name = (String) arr.get(0);
var type = PipelineType.values()[(Integer) arr.get(1) + 2];
var newPipelineEvent =
new IncomingWebSocketEvent<>(
@@ -310,6 +312,18 @@ public class SocketHandler {
}
break;
}
case SMT_CHANGEPIPELINETYPE:
{
var changePipelineEvent =
new IncomingWebSocketEvent<>(
DataChangeDestination.DCD_ACTIVEMODULE,
"changePipelineType",
(Integer) entryValue,
cameraIndex,
context);
dcService.publishEvent(changePipelineEvent);
break;
}
}
} catch (Exception e) {
logger.error("Failed to parse message!", e);

View File

@@ -35,7 +35,8 @@ public enum SocketMessageType {
SMT_TAKECALIBRATIONSNAPSHOT("takeCalibrationSnapshot"),
SMT_DUPLICATEPIPELINE("duplicatePipeline"),
SMT_CHANGEBRIGHTNESS("enabledLEDPercentage"),
SMT_ROBOTOFFSETPOINT("robotOffsetPoint");
SMT_ROBOTOFFSETPOINT("robotOffsetPoint"),
SMT_CHANGEPIPELINETYPE("pipelineType");
public final String entryKey;

View File

@@ -26,7 +26,11 @@ public class Packet {
// Read and write positions.
int readPos, writePos;
/** Constructs an empty packet. */
/**
* Constructs an empty packet.
*
* @param size The size of the packet buffer.
*/
public Packet(int size) {
this.size = size;
packetData = new byte[size];

View File

@@ -33,9 +33,6 @@ public class PhotonPipelineResult {
// Latency in milliseconds.
private double latencyMillis;
// Whether targets exist.
private boolean hasTargets;
/** Constructs an empty pipeline result. */
public PhotonPipelineResult() {}
@@ -47,7 +44,6 @@ public class PhotonPipelineResult {
*/
public PhotonPipelineResult(double latencyMillis, List<PhotonTrackedTarget> targets) {
this.latencyMillis = latencyMillis;
this.hasTargets = targets.size() != 0;
this.targets.addAll(targets);
}
@@ -67,7 +63,7 @@ public class PhotonPipelineResult {
* @return The best target of the pipeline result.
*/
public PhotonTrackedTarget getBestTarget() {
if (!hasTargets && !HAS_WARNED) {
if (!hasTargets() && !HAS_WARNED) {
String errStr =
"This PhotonPipelineResult object has no targets associated with it! Please check hasTargets() "
+ "before calling this method. For more information, please review the PhotonLib "
@@ -76,7 +72,7 @@ public class PhotonPipelineResult {
new Exception().printStackTrace();
HAS_WARNED = true;
}
return hasTargets ? targets.get(0) : null;
return hasTargets() ? targets.get(0) : null;
}
/**
@@ -94,7 +90,7 @@ public class PhotonPipelineResult {
* @return Whether the pipeline has targets.
*/
public boolean hasTargets() {
return hasTargets;
return targets.size() > 0;
}
/**
@@ -112,14 +108,13 @@ public class PhotonPipelineResult {
if (o == null || getClass() != o.getClass()) return false;
PhotonPipelineResult that = (PhotonPipelineResult) o;
boolean latencyMatch = Double.compare(that.latencyMillis, latencyMillis) == 0;
boolean hasTargetsMatch = that.hasTargets == hasTargets;
boolean targetsMatch = that.targets.equals(targets);
return latencyMatch && hasTargetsMatch && targetsMatch;
return latencyMatch && targetsMatch;
}
@Override
public int hashCode() {
return Objects.hash(latencyMillis, hasTargets, targets);
return Objects.hash(latencyMillis, targets);
}
/**
@@ -131,7 +126,6 @@ public class PhotonPipelineResult {
public Packet createFromPacket(Packet packet) {
// Decode latency, existence of targets, and number of targets.
latencyMillis = packet.decodeDouble();
hasTargets = packet.decodeBoolean();
byte targetCount = packet.decodeByte();
targets.clear();
@@ -155,7 +149,6 @@ public class PhotonPipelineResult {
public Packet populatePacket(Packet packet) {
// Encode latency, existence of targets, and number of targets.
packet.encode(latencyMillis);
packet.encode(hasTargets);
packet.encode((byte) targets.size());
// Encode the information of each target.

View File

@@ -54,6 +54,10 @@ public class PhotonTrackedTarget {
return area;
}
public double getSkew() {
return skew;
}
public Transform2d getCameraToTarget() {
return cameraToTarget;
}

View File

@@ -79,7 +79,7 @@ public class Robot extends TimedRobot {
CAMERA_HEIGHT_METERS,
TARGET_HEIGHT_METERS,
CAMERA_PITCH_RADIANS,
result.getBestTarget().getPitch());
Units.degreesToRadians(result.getBestTarget().getPitch()));
// Use this range as the measurement we give to the PID controller.
// -1.0 required to ensure positive PID controller effort _increases_ range

View File

@@ -81,7 +81,7 @@ public class Robot extends TimedRobot {
CAMERA_HEIGHT_METERS,
TARGET_HEIGHT_METERS,
CAMERA_PITCH_RADIANS,
result.getBestTarget().getPitch());
Units.degreesToRadians(result.getBestTarget().getPitch()));
// Use this range as the measurement we give to the PID controller.
// -1.0 required to ensure positive PID controller effort _increases_ range

View File

@@ -42,7 +42,7 @@ dependencies {
compile "edu.wpi.first.thirdparty.frc2021.opencv:opencv-jni:$opencvVersion:osxx86-64"
compile "edu.wpi.first.thirdparty.frc2021.opencv:opencv-jni:$opencvVersion:windowsx86-64"
implementation "edu.wpi.first.wpimath:wpimath-java:2021.1.2-9-g26584ff"
implementation "edu.wpi.first.wpimath:wpimath-java:2021.3.1"
// test stuff
testImplementation("org.junit.jupiter:junit-jupiter:5.6.0")
@@ -53,12 +53,14 @@ test {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
workingDir = new File("${rootDir}")
}
task testHeadless(type: Test) {
group = "verification"
systemProperty("java.awt.headless", "true")
useJUnitPlatform()
exclude '**/*BenchmarkTest*'
}
task generateJavaDocs(type: Javadoc) {
@@ -68,7 +70,7 @@ task generateJavaDocs(type: Javadoc) {
}
jacocoTestReport {
dependsOn test // Tests are required to run before generating the report
// dependsOn testHeadless // Tests are required to run before generating the report
reports {
xml.enabled true

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Some files were not shown because too many files have changed in this diff Show More