Compare commits

...

16 Commits

Author SHA1 Message Date
Matt
a731c7a8db Revert part of #288 (#306)
Fixes picam streaming/hangs
2021-10-30 12:27:41 -04:00
Matt
7e74da5cff Make photonlib complain if versions don't match (#302)
Adds messages if Photon isn't detected or major versions don't match
2021-10-18 22:31:18 -04:00
Matt
0977fd0dff Update PacketTest.java (#301)
Adds unit test to make sure the packet structure doesn't change
2021-10-16 09:42:21 -04:00
Matt
3241ef7b1b Update dev tag matcher (#300) 2021-10-07 11:13:11 -04:00
Matt
f922466d41 Fix contour grouping (#298)
Fixes bug where n+1 contours were needed to match a target of size n
2021-10-05 12:16:50 -04:00
Matt
243f06da2d Fix vision module stream index logic (#295)
Previous logic could cause stream index with multiple cameras to run away
2021-10-03 22:43:39 -04:00
Matt
44e91a184d Publish photon-targeting (#292)
Pushes photon-targeting to Maven
2021-10-03 10:58:35 -04:00
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
384 changed files with 1634 additions and 898 deletions

View File

@@ -72,15 +72,15 @@ jobs:
- name: Gradle Build
run: |
chmod +x gradlew
./gradlew build -x check
./gradlew build -x check --max-workers 1
# Run Gradle Tests.
- name: Gradle Tests
run: ./gradlew testHeadless -i
run: ./gradlew testHeadless -i --max-workers 1
# Generate Coverage Report.
- name: Gradle Coverage
run: ./gradlew jacocoTestReport
run: ./gradlew jacocoTestReport --max-workers 1
# Publish Coverage Report.
- name: Publish Server Coverage Report
@@ -147,6 +147,7 @@ jobs:
chmod +x gradlew
./gradlew spotlessCheck
photon-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [photon-build-package]
@@ -186,8 +187,8 @@ jobs:
- run: git fetch --tags --force
- run: |
chmod +x gradlew
./gradlew photon-lib:build
- run: ./gradlew photon-lib:publish
./gradlew photon-lib:build --max-workers 1
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
@@ -215,11 +216,9 @@ jobs:
- uses: actions/setup-java@v1
with:
java-version: 11
- run: |
git describe --tags --exclude="Dev"
- run: |
chmod +x gradlew
./gradlew photon-lib:build
./gradlew photon-lib:build --max-workers 1
- run: |
chmod +x gradlew
./gradlew photon-lib:publish
@@ -293,7 +292,7 @@ jobs:
# Build fat jar.
- run: |
chmod +x gradlew
./gradlew photon-server:shadowJar
./gradlew photon-server:shadowJar --max-workers 1
# Upload final fat jar as artifact.
- uses: actions/upload-artifact@master

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
@@ -41,5 +41,6 @@ spotless {
target "**/*.java"
licenseHeaderFile "$rootDir/LicenseHeader.txt"
targetExclude("photon-core/src/main/java/org/photonvision/PhotonVersion.java")
targetExclude("photon-lib/src/main/java/org/photonvision/PhotonVersion.java")
}
}

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

@@ -1,5 +1,4 @@
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.nio.file.Path
apply from: "${rootDir}/shared/common.gradle"
@@ -30,23 +29,8 @@ dependencies {
}
task writeCurrentVersionJava {
String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now())
File versionFile = new File(java.nio.file.Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java")
.toAbsolutePath().toString())
versionFile.delete()
versionFile << "package org.photonvision;\n" +
"\n" +
"/*\n" +
" * Autogenerated file! Do not manually edit this file. This version is regenerated\n" +
" * any time the publish task is run, or when this file is deleted.\n" +
" */\n" +
"\n" +
"@SuppressWarnings(\"ALL\")\n" +
"public final class PhotonVersion {\n" +
" public static final String versionString = \"${versionString}\";\n" +
" public static final String buildDate = \"${date}\";\n" +
" public static final boolean isRelease = !versionString.startsWith(\"dev\");\n" +
"}"
writePhotonVersionFile(Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
versionString)
}
build.dependsOn writeCurrentVersionJava

View File

@@ -21,6 +21,7 @@ import edu.wpi.first.networktables.LogMessage;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableInstance;
import java.util.function.Consumer;
import org.photonvision.PhotonVersion;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@@ -62,16 +63,23 @@ public class NetworkTablesManager {
hasReportedConnectionFailure = false;
lastConnectMessageMillis = System.currentTimeMillis();
ScriptManager.queueEvent(ScriptEventType.kNTConnected);
getInstance().broadcastVersion();
}
}
}
private void broadcastVersion() {
kRootTable.getEntry("version").setString(PhotonVersion.versionString);
kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate);
}
public void setConfig(NetworkConfig config) {
if (config.runNTServer) {
setServerMode();
} else {
setClientMode(config.teamNumber);
}
broadcastVersion();
}
private void setClientMode(int teamNumber) {
@@ -86,11 +94,13 @@ public class NetworkTablesManager {
logger.error(
"[NetworkTablesManager] Could not connect to the robot! Will retry in the background...");
}
broadcastVersion();
}
private void setServerMode() {
logger.info("Starting NT Server");
ntInstance.stopClient();
ntInstance.startServer();
broadcastVersion();
}
}

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((testMode ? "src/main" : "src/test"), "resources").toAbsolutePath();
return Path.of(testMode ? "src/main/resources" : "../test-resources").toAbsolutePath();
}
public static Path getTestMode2019ImagePath() {

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

@@ -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

@@ -46,7 +46,7 @@ public class GroupContoursPipe
} else {
int groupingCount = params.getGroup().count;
if (input.size() > groupingCount) {
if (input.size() >= groupingCount) {
input.sort(Contour.SortByMomentsX);
// also why reverse? shouldn't the sort comparator just get reversed?
// TODO: Matt, see this

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,43 @@ 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();
// Otherwise, the input mat is frame we got from the camera
rawInputMat = frame.image.getMat();
}
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 +249,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 +265,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 +278,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 +295,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

@@ -166,6 +166,8 @@ public class ReflectivePipeline extends CVPipeline<CVPipelineResult, ReflectiveP
} 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

@@ -19,7 +19,6 @@ package org.photonvision.vision.processes;
import java.util.*;
import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration;
/** VisionModuleManager has many VisionModules, and provides camera configuration data to them. */
public class VisionModuleManager {
@@ -54,9 +53,9 @@ public class VisionModuleManager {
public List<VisionModule> addSources(List<VisionSource> visionSources) {
var addedModules = new HashMap<Integer, VisionModule>();
assignCameraIndex(visionSources);
for (var visionSource : visionSources) {
var pipelineManager = new PipelineManager(visionSource.getCameraConfiguration());
assignCameraIndex(visionSource.getCameraConfiguration());
var module = new VisionModule(pipelineManager, visionSource, visionModules.size());
visionModules.add(module);
@@ -72,16 +71,30 @@ public class VisionModuleManager {
return sortedModulesList;
}
private void assignCameraIndex(CameraConfiguration config) {
var max =
visionModules.stream()
.mapToInt(it -> it.visionSource.getCameraConfiguration().streamIndex)
.max()
.orElse(-1);
private void assignCameraIndex(List<VisionSource> config) {
// We won't necessarily have already added all of the cameras we need to at this point
// But by operating on the list, we have a fairly good idea of which we need to change
// but it's not guaranteed that we change the correct one
// The best we can do is try to avoid a case where the stream index runs away to infinity
// since we can only stream 5 cameras at once
// If the current stream index is reserved, increase by 1
if (config.streamIndex <= max) {
config.streamIndex = max + 1;
for (var v : config) {
var listNoV = new ArrayList<>(config);
listNoV.remove(v);
if (listNoV.stream()
.anyMatch(
it ->
it.getCameraConfiguration().streamIndex
== v.getCameraConfiguration().streamIndex)) {
int idx = 0;
while (listNoV.stream()
.map(it -> it.getCameraConfiguration().streamIndex)
.collect(Collectors.toList())
.contains(idx)) {
idx++;
}
v.getCameraConfiguration().streamIndex = idx;
}
}
}
}

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;
}

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

@@ -19,6 +19,7 @@ package org.photonvision.vision.pipeline;
import static org.junit.jupiter.api.Assertions.*;
import edu.wpi.first.wpilibj.geometry.Rotation2d;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -30,7 +31,9 @@ import org.photonvision.vision.frame.provider.FileFrameProvider;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.opencv.ContourGroupingMode;
import org.photonvision.vision.opencv.ContourIntersectionDirection;
import org.photonvision.vision.opencv.ContourShape;
import org.photonvision.vision.pipeline.result.CVPipelineResult;
import org.photonvision.vision.target.TargetModel;
import org.photonvision.vision.target.TrackedTarget;
public class CirclePNPTest {
@@ -80,46 +83,43 @@ public class CirclePNPTest {
assertEquals(5, cameraCalibration.getCameraExtrinsicsMat().cols());
}
// @Test
// public void testCircle() {
// var pipeline = new ColoredShapePipeline();
//
// pipeline.getSettings().hsvHue.set(0, 100);
// pipeline.getSettings().hsvSaturation.set(100, 255);
// pipeline.getSettings().hsvValue.set(100, 255);
// pipeline.getSettings().outputShouldDraw = true;
// pipeline.getSettings().maxCannyThresh = 50;
// pipeline.getSettings().accuracy = 15;
// pipeline.getSettings().allowableThreshold = 5;
// pipeline.getSettings().solvePNPEnabled = true;
// pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
// pipeline.getSettings().cornerDetectionUseConvexHulls = true;
// pipeline.getSettings().cameraCalibration = getCoeffs(LIFECAM_480P_CAL_FILE);
// pipeline.getSettings().targetModel = TargetModel.kCircularPowerCell7in;
// pipeline.getSettings().outputShouldDraw = true;
// 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().accuracyPercentage = 30.0;
//
// var frameProvider =
// new FileFrameProvider(
//
// TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_6, false),
// TestUtils.WPI2020Image.FOV,
// new Rotation2d(),
// TestUtils.get2020LifeCamCoeffs(false));
//
// CVPipelineResult pipelineResult = pipeline.run(frameProvider.get(),
// QuirkyCamera.DefaultCamera);
// printTestResults(pipelineResult);
//
// TestUtils.showImage(pipelineResult.outputFrame.image.getMat(), "Pipeline output",
// 999999);
// }
@Test
public void testCircle() {
var pipeline = new ColoredShapePipeline();
pipeline.getSettings().hsvHue.set(0, 100);
pipeline.getSettings().hsvSaturation.set(100, 255);
pipeline.getSettings().hsvValue.set(100, 255);
pipeline.getSettings().outputShouldDraw = true;
pipeline.getSettings().maxCannyThresh = 50;
pipeline.getSettings().circleAccuracy = 15;
pipeline.getSettings().circleDetectThreshold = 5;
pipeline.getSettings().solvePNPEnabled = true;
pipeline.getSettings().cornerDetectionAccuracyPercentage = 4;
pipeline.getSettings().cornerDetectionUseConvexHulls = true;
pipeline.getSettings().cameraCalibration = getCoeffs(LIFECAM_480P_CAL_FILE);
pipeline.getSettings().targetModel = TargetModel.kCircularPowerCell7in;
pipeline.getSettings().outputShouldDraw = true;
pipeline.getSettings().outputShowMultipleTargets = false;
pipeline.getSettings().contourGroupingMode = ContourGroupingMode.Single;
pipeline.getSettings().contourIntersection = ContourIntersectionDirection.Up;
pipeline.getSettings().contourShape = ContourShape.Circle;
pipeline.getSettings().circleDetectThreshold = 10;
pipeline.getSettings().contourRadius.setFirst(30);
pipeline.getSettings().accuracyPercentage = 30.0;
var frameProvider =
new FileFrameProvider(
TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_6, false),
TestUtils.WPI2020Image.FOV,
new Rotation2d(),
TestUtils.get2020LifeCamCoeffs(true));
CVPipelineResult pipelineResult = pipeline.run(frameProvider.get(), QuirkyCamera.DefaultCamera);
printTestResults(pipelineResult);
TestUtils.showImage(pipelineResult.outputFrame.image.getMat(), "Pipeline output", 999999);
}
private static void continuouslyRunPipeline(Frame frame, ReflectivePipelineSettings settings) {
var pipeline = new ReflectivePipeline();

View File

@@ -17,6 +17,7 @@
package org.photonvision.vision.pipeline;
import org.junit.jupiter.api.Test;
import org.photonvision.common.util.TestUtils;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
@@ -39,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(
@@ -49,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(
@@ -57,36 +58,33 @@ public class ColoredShapePipelineTest {
printTestResults(colouredShapePipelineResult);
}
// @Test
// public static void testCircleShapeDetection(
// ColoredShapePipeline pipeline, ColoredShapePipelineSettings settings, Frame frame) {
// settings.desiredShape = ContourShape.Circle;
// pipeline.settings = settings;
// CVPipelineResult colouredShapePipelineResult = pipeline.run(frame,
// QuirkyCamera.DefaultCamera);
// TestUtils.showImage(
// colouredShapePipelineResult.outputFrame.image.getMat(), "Pipeline output:
// Circle.");
// printTestResults(colouredShapePipelineResult);
// }
//
// @Test
// public static void testPowercellDetection(
// ColoredShapePipelineSettings settings, ColoredShapePipeline pipeline) {
//
// settings.hsvHue.set(10, 40);
// settings.hsvSaturation.set(100, 255);
// settings.hsvValue.set(100, 255);
// settings.maxCannyThresh = 50;
// settings.accuracy = 15;
// settings.allowableThreshold = 5;
// var frameProvider =
// new FileFrameProvider(
//
// TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_6, false),
// TestUtils.WPI2019Image.FOV);
// testCircleShapeDetection(pipeline, settings, frameProvider.get());
// }
@Test
public static void testCircleShapeDetection(
ColoredShapePipeline pipeline, ColoredShapePipelineSettings settings, Frame frame) {
settings.contourShape = ContourShape.Circle;
pipeline.settings = settings;
CVPipelineResult colouredShapePipelineResult = pipeline.run(frame, QuirkyCamera.DefaultCamera);
TestUtils.showImage(
colouredShapePipelineResult.outputFrame.image.getMat(), "Pipeline output: Circle.");
printTestResults(colouredShapePipelineResult);
}
@Test
public static void testPowercellDetection(
ColoredShapePipelineSettings settings, ColoredShapePipeline pipeline) {
settings.hsvHue.set(10, 40);
settings.hsvSaturation.set(100, 255);
settings.hsvValue.set(100, 255);
settings.maxCannyThresh = 50;
settings.circleAccuracy = 15;
settings.circleDetectThreshold = 5;
var frameProvider =
new FileFrameProvider(
TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_6, false),
TestUtils.WPI2019Image.FOV);
testCircleShapeDetection(pipeline, settings, frameProvider.get());
}
public static void main(String[] args) {
TestUtils.loadLibraries();
@@ -104,8 +102,8 @@ 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();

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

@@ -17,10 +17,14 @@
package org.photonvision.vision.processes;
import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.cscore.VideoMode;
import edu.wpi.first.wpilibj.geometry.Rotation2d;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.*;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
@@ -131,6 +135,52 @@ public class VisionModuleManagerTest {
printTestResults(module0DataConsumer.result);
}
@Test
public void testMultipleStreamIndex() {
ConfigManager.getInstance().load();
var conf = new CameraConfiguration("Foo", "Bar");
conf.streamIndex = 1;
var ffp =
new FileFrameProvider(
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),
TestUtils.WPI2019Image.FOV);
var testSource = new TestSource(ffp, conf);
var conf2 = new CameraConfiguration("Foo2", "Bar");
conf2.streamIndex = 0;
var ffp2 =
new FileFrameProvider(
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),
TestUtils.WPI2019Image.FOV);
var testSource2 = new TestSource(ffp2, conf2);
var conf3 = new CameraConfiguration("Foo3", "Bar");
conf3.streamIndex = 0;
var ffp3 =
new FileFrameProvider(
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),
TestUtils.WPI2019Image.FOV);
var testSource3 = new TestSource(ffp3, conf3);
var modules =
VisionModuleManager.getInstance().addSources(List.of(testSource, testSource2, testSource3));
System.out.println(
Arrays.toString(
modules.stream()
.map(it -> it.visionSource.getCameraConfiguration().streamIndex)
.collect(Collectors.toList())
.toArray()));
var idxs =
modules.stream()
.map(it -> it.visionSource.getCameraConfiguration().streamIndex)
.collect(Collectors.toList());
assertTrue(idxs.contains(0));
assertTrue(idxs.contains(1));
assertTrue(idxs.contains(2));
}
private static void printTestResults(CVPipelineResult pipelineResult) {
double fps = 1000 / pipelineResult.getLatencyMillis();
System.out.print(

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

@@ -1,14 +1,19 @@
import java.nio.file.Path
apply plugin: "cpp"
apply plugin: "java"
apply plugin: "google-test-test-suite"
apply plugin: "edu.wpi.first.NativeUtils"
apply from: "${rootDir}/shared/config.gradle"
apply from: "${rootDir}/versioningHelper.gradle"
test {
useJUnitPlatform()
}
def jniPlatforms = ['linuxaarch64bionic', 'linuxraspbian', 'linuxx86-64', 'osxx86-64', 'windowsx86-64']
// Apply Java configuration
dependencies {
compile project(":photon-targeting")
@@ -19,16 +24,15 @@ dependencies {
implementation "edu.wpi.first.wpilibj:wpilibj-java:$wpilibVersion"
implementation "edu.wpi.first.wpiutil:wpiutil-java:$wpilibVersion"
implementation "edu.wpi.first.wpimath:wpimath-java:$wpilibVersion"
implementation "edu.wpi.first.hal:hal-java:$wpilibVersion"
implementation "edu.wpi.first.thirdparty.frc2020.opencv:opencv-java:3.4.7-2"
// NTCore
implementation "edu.wpi.first.ntcore:ntcore-java:$wpilibVersion"
compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:linuxaarch64bionic"
compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:linuxraspbian"
compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:linuxx86-64"
compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:osxx86-64"
compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:windowsx86-64"
jniPlatforms.each { compile "edu.wpi.first.ntcore:ntcore-jni:$wpilibVersion:$it" }
// HAL
implementation "edu.wpi.first.hal:hal-java:$wpilibVersion"
jniPlatforms.each {compile "edu.wpi.first.hal:hal-jni:$wpilibVersion:$it"}
// Junit
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
@@ -102,4 +106,12 @@ task generateVendorJson() {
build.dependsOn generateVendorJson
task writeCurrentVersionJava {
writePhotonVersionFile(Path.of("$projectDir", "src", "main", "java", "org", "photonvision", "PhotonVersion.java"),
versionString)
}
build.dependsOn writeCurrentVersionJava
apply from: "publish.gradle"

View File

@@ -141,6 +141,7 @@ publishing {
username 'ghactions'
password System.getenv("ARTIFACTORY_API_KEY")
}
println("Publishing to " + url)
}
}
}

View File

@@ -7,7 +7,7 @@
"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": [
{

View File

@@ -20,6 +20,7 @@ package org.photonvision;
import edu.wpi.first.networktables.NetworkTable;
import edu.wpi.first.networktables.NetworkTableEntry;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.wpilibj.DriverStation;
import org.photonvision.common.dataflow.structures.Packet;
import org.photonvision.common.hardware.VisionLEDMode;
import org.photonvision.targeting.PhotonPipelineResult;
@@ -32,8 +33,10 @@ public class PhotonCamera {
final NetworkTableEntry outputSaveImgEntry;
final NetworkTableEntry pipelineIndexEntry;
final NetworkTableEntry ledModeEntry;
final NetworkTableEntry versionEntry;
final NetworkTable mainTable = NetworkTableInstance.getDefault().getTable("photonvision");
private final String path;
boolean driverMode;
int pipelineIndex;
@@ -47,12 +50,14 @@ public class PhotonCamera {
* @param rootTable The root table that the camera is broadcasting information over.
*/
public PhotonCamera(NetworkTable rootTable) {
path = rootTable.getPath();
rawBytesEntry = rootTable.getEntry("rawBytes");
driverModeEntry = rootTable.getEntry("driverMode");
inputSaveImgEntry = rootTable.getEntry("inputSaveImgCmd");
outputSaveImgEntry = rootTable.getEntry("outputSaveImgCmd");
pipelineIndexEntry = rootTable.getEntry("pipelineIndex");
ledModeEntry = mainTable.getEntry("ledMode");
versionEntry = mainTable.getEntry("version");
driverMode = driverModeEntry.getBoolean(false);
pipelineIndex = pipelineIndexEntry.getNumber(0).intValue();
@@ -74,6 +79,8 @@ public class PhotonCamera {
* @return The latest pipeline result.
*/
public PhotonPipelineResult getLatestResult() {
verifyVersion();
// Clear the packet.
packet.clear();
@@ -195,7 +202,24 @@ 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();
}
private void verifyVersion() {
String versionString = versionEntry.getString("");
if (versionString.equals("")) {
DriverStation.reportError(
"PhotonVision coprocessor at path " + path + " not found on NetworkTables!", true);
} else if (!PhotonVersion.versionMatches(versionString)) {
DriverStation.reportError(
"Photon version "
+ PhotonVersion.versionString
+ " does not match coprocessor version "
+ versionString
+ "!",
true);
}
}
}

View File

@@ -24,7 +24,6 @@ public class SimVisionTarget {
double targetWidthMeters;
double targetHeightMeters;
double targetHeightAboveGroundMeters;
double targetInfill_pct;
double tgtAreaMeters2;
/**

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

@@ -95,4 +95,29 @@ class PacketTest {
Assertions.assertEquals(t, target);
}
@Test
void testPacketv2021_1_6() {
// From v2021.1.6
var simplified =
new PhotonPipelineResult(
12.34,
List.of(
new PhotonTrackedTarget(
-23, -10, 6, 1, new Transform2d(new Translation2d(1, 2), new Rotation2d(3)))));
byte[] bytes = {
64, 40, -82, 20, 122, -31, 71, -82, 1, -64, 55, 0, 0, 0, 0, 0, 0, -64, 36, 0, 0, 0, 0, 0, 0,
64, 24, 0, 0, 0, 0, 0, 0, 63, -16, 0, 0, 0, 0, 0, 0, 63, -16, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0,
0, 0, 0, 0, 64, 101, 124, 101, 19, -54, -47, 122, 0
};
// Let's check that those bytes still mean the same thing
Packet packet = new Packet(1);
packet.clear();
packet.setData(bytes);
var ret = new PhotonPipelineResult();
ret.createFromPacket(packet);
System.out.println(ret);
Assertions.assertEquals(simplified, ret);
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class PhotonVersionTest {
public static final boolean versionMatches(String versionString, String other) {
String c = versionString;
Pattern p = Pattern.compile("v[0-9]+.[0-9]+.[0-9]+");
Matcher m = p.matcher(c);
if (m.find()) {
c = m.group(0);
} else {
return false;
}
m = p.matcher(other);
if (m.find()) {
other = m.group(0);
} else {
return false;
}
return c.equals(other);
}
@Test
public void testVersion() {
Assertions.assertTrue(versionMatches("v2021.1.6", "v2021.1.6"));
Assertions.assertTrue(versionMatches("dev-v2021.1.6", "v2021.1.6"));
Assertions.assertTrue(versionMatches("dev-v2021.1.6-5-gca49ea50", "v2021.1.6"));
Assertions.assertFalse(versionMatches("", "v2021.1.6"));
Assertions.assertFalse(versionMatches("v2021.1.6", ""));
}
}

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;
@@ -123,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

@@ -0,0 +1,51 @@
/*
* 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;
/*
* Autogenerated file! Do not manually edit this file. This version is regenerated
* any time the publish task is run, or when this file is deleted.
*/
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings("ALL")
public final class PhotonVersion {
public static final String versionString = "${version}";
public static final String buildDate = "${date}";
public static final boolean isRelease = !versionString.startsWith("dev");
public static final boolean versionMatches(String other) {
String c = versionString;
Pattern p = Pattern.compile("v[0-9]+.[0-9]+.[0-9]+");
Matcher m = p.matcher(c);
if (m.find()) {
c = m.group(0);
} else {
return false;
}
m = p.matcher(other);
if (m.find()) {
other = m.group(0);
} else {
return false;
}
return c.equals(other);
}
}

View File

@@ -1,3 +1,7 @@
import java.nio.file.Path
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
// Plugins
apply plugin: "jacoco"
apply plugin: "java"
@@ -42,7 +46,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")

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

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

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