Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a731c7a8db | ||
|
|
7e74da5cff | ||
|
|
0977fd0dff | ||
|
|
3241ef7b1b | ||
|
|
f922466d41 | ||
|
|
243f06da2d | ||
|
|
44e91a184d | ||
|
|
e6b0f398b6 | ||
|
|
5a475e1071 | ||
|
|
f8def88e4d | ||
|
|
db09e5209f | ||
|
|
9fdd945a52 | ||
|
|
00b8e7d1c5 | ||
|
|
798b8e398a | ||
|
|
affb27038b | ||
|
|
6767781a41 |
17
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
10
photon-client/src/mixins/global/stateMixin.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const stateMixin = {
|
||||
methods: {
|
||||
currentPipelineType() {
|
||||
return this.$store.getters.pipelineType
|
||||
},
|
||||
currentPipelineSettings() {
|
||||
return this.$store.getters.currentPipelineSettings
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,6 +45,8 @@ public class SpeckleRejectPipe
|
||||
for (Contour c : in) {
|
||||
if (c.getArea() >= minAllowedArea) {
|
||||
m_despeckledContours.add(c);
|
||||
} else {
|
||||
c.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -15,6 +15,7 @@ includeProject {
|
||||
|
||||
includeOtherLibs {
|
||||
^frc/
|
||||
^networktables/
|
||||
^units/
|
||||
^wpi/
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -141,6 +141,7 @@ publishing {
|
||||
username 'ghactions'
|
||||
password System.getenv("ARTIFACTORY_API_KEY")
|
||||
}
|
||||
println("Publishing to " + url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ public class SimVisionTarget {
|
||||
double targetWidthMeters;
|
||||
double targetHeightMeters;
|
||||
double targetHeightAboveGroundMeters;
|
||||
double targetInfill_pct;
|
||||
double tgtAreaMeters2;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", ""));
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,4 @@
|
||||
#include "gtest/gtest.h"
|
||||
#include "photonlib/PhotonUtils.h"
|
||||
|
||||
TEST(PhotonUtilsTest, TestInclude) {}
|
||||
TEST(PhotonUtilsTest, Include) {}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
@@ -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];
|
||||
|
||||
@@ -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.
|
||||
|
||||
51
shared/PhotonVersion.java.in
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |