Matt
2024-01-05 12:26:17 -07:00
committed by GitHub
parent b033f7e585
commit 0af5a62d5e
59 changed files with 1327 additions and 266 deletions

View File

@@ -78,6 +78,8 @@ jobs:
with:
java-version: 17
distribution: temurin
- name: Install mrcal deps
run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
- name: Gradle Build
run: |
chmod +x gradlew

View File

@@ -93,6 +93,14 @@ Our meeting notes can be found in the wiki section of this repository.
* [2020 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2020-Meeting-Notes)
* [2021 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2021-Meeting-Notes)
## Additional packages
For now, using mrcal requires installing these additional packages on Linux systems:
```
sudo apt install libcholmod3 liblapack3 libsuitesparseconfig5
```
## Documentation
- Our main documentation page: [docs.photonvision.org](https://docs.photonvision.org)

View File

@@ -29,18 +29,21 @@ ext {
javalinVersion = "5.6.2"
photonGlDriverLibVersion = "dev-v2023.1.0-9-g75fc678"
frcYear = "2024"
mrcalVersion = "dev-v2024.0.0-7-gc976aaa";
pubVersion = versionString
isDev = pubVersion.startsWith("dev")
// A list, for legacy reasons, with only the current platform contained
String nativeName = wpilibTools.platformMapper.currentPlatform.platformName;
if (nativeName == "linuxx64") nativeName = "linuxx86-64";
if (nativeName == "winx64") nativeName = "windowsx86-64";
if (nativeName == "macx64") nativeName = "osxx86-64";
if (nativeName == "macarm64") nativeName = "osxarm64";
wpilibNativeName = wpilibTools.platformMapper.currentPlatform.platformName;
def nativeName = wpilibNativeName
if (wpilibNativeName == "linuxx64") nativeName = "linuxx86-64";
if (wpilibNativeName == "winx64") nativeName = "windowsx86-64";
if (wpilibNativeName == "macx64") nativeName = "osxx86-64";
if (wpilibNativeName == "macarm64") nativeName = "osxarm64";
jniPlatform = nativeName
println("Building for platform: " + jniPlatform)
println("Building for platform " + jniPlatform + " wpilib: " + wpilibNativeName)
println("Using Wpilib: " + wpilibVersion)
println("Using OpenCV: " + openCVversion)
}

View File

@@ -1,13 +1,17 @@
import argparse
import base64
from dataclasses import dataclass
import json
import os
from typing import Union
import cv2
import numpy as np
import mrcal
from wpimath.geometry import Quaternion as _Quat
@dataclass
class Resolution:
class Size:
width: int
height: int
@@ -86,10 +90,98 @@ class Observation:
@dataclass
class CameraCalibration:
resolution: Resolution
resolution: Size
cameraIntrinsics: JsonMatOfDoubles
distCoeffs: JsonMatOfDoubles
observations: list[Observation]
calobjectWarp: list[float]
calobjectSize: Size
calobjectSpacing: float
def __convert_cal_to_mrcal_cameramodel(
cal: CameraCalibration,
) -> mrcal.cameramodel | None:
if len(cal.distCoeffs.data) == 5:
model = "LENSMODEL_OPENCV5"
elif len(cal.distCoeffs.data) == 8:
model = "LENSMODEL_OPENCV8"
else:
print("Unknown camera model? giving up")
return None
def opencv_to_mrcal_intrinsics(ocv):
return [ocv[0], ocv[4], ocv[2], ocv[5]]
def pose_to_rt(pose: Pose3d):
r = _Quat(
w=pose.rotation.quaternion.W,
x=pose.rotation.quaternion.X,
y=pose.rotation.quaternion.Y,
z=pose.rotation.quaternion.Z,
).toRotationVector()
t = [
pose.translation.x,
pose.translation.y,
pose.translation.z,
]
return np.concatenate((r, t))
imagersize = (cal.resolution.width, cal.resolution.height)
# Always weight=1 for Photon data
WEIGHT = 1
observations_board = np.array(
[
# note that we expect row-major observations here. I think this holds
np.array(
list(map(lambda it: [it.x, it.y, WEIGHT], o.locationInImageSpace))
).reshape((cal.calobjectSize.width, cal.calobjectSize.height, 3))
for o in cal.observations
]
)
optimization_inputs = {
"intrinsics": np.array(
[
opencv_to_mrcal_intrinsics(cal.cameraIntrinsics.data)
+ cal.distCoeffs.data
],
dtype=np.float64,
),
"extrinsics_rt_fromref": np.zeros((0, 6), dtype=np.float64),
"frames_rt_toref": np.array(
[pose_to_rt(o.optimisedCameraToObject) for o in cal.observations]
),
"points": None,
"observations_board": observations_board,
"indices_frame_camintrinsics_camextrinsics": np.array(
[[i, 0, -1] for i in range(len(cal.observations))], dtype=np.int32
),
"observations_point": None,
"indices_point_camintrinsics_camextrinsics": None,
"lensmodel": model,
"imagersizes": np.array([imagersize], dtype=np.int32),
"calobject_warp": np.array(cal.calobjectWarp)
if len(cal.calobjectWarp) > 0
else None,
# We always do all the things
"do_optimize_intrinsics_core": True,
"do_optimize_intrinsics_distortions": True,
"do_optimize_extrinsics": True,
"do_optimize_frames": True,
"do_optimize_calobject_warp": len(cal.calobjectWarp) > 0,
"do_apply_outlier_rejection": True,
"do_apply_regularization": True,
"verbose": False,
"calibration_object_spacing": cal.calobjectSpacing,
"imagepaths": np.array([it.snapshotName for it in cal.observations]),
}
return mrcal.cameramodel(
optimization_inputs=optimization_inputs,
icam_intrinsics=0,
)
def convert_photon_to_mrcal(photon_cal_json_path: str, output_folder: str):
@@ -132,3 +224,32 @@ def convert_photon_to_mrcal(photon_cal_json_path: str, output_folder: str):
vnl_file.write(f"{obs.snapshotName} {corner.x} {corner.y} 0\n")
vnl_file.flush()
mrcal_model = __convert_cal_to_mrcal_cameramodel(camera_cal_data)
with open(f"{output_folder}/camera-0.cameramodel", "w+") as mrcal_file:
mrcal_model.write(
mrcal_file,
note="Generated from PhotonVision calibration file: "
+ photon_cal_json_path
+ "\nCalobject_warp (m): "
+ str(camera_cal_data.calobjectWarp),
)
def main():
parser = argparse.ArgumentParser(
description="Convert Photon calibration JSON for use with mrcal"
)
parser.add_argument("input", type=str, help="Path to Photon calibration JSON file")
parser.add_argument(
"output_folder", type=str, help="Output folder for mrcal VNL file + images"
)
args = parser.parse_args()
convert_photon_to_mrcal(args.input, args.output_folder)
if __name__ == "__main__":
main()

View File

@@ -13,6 +13,7 @@ import PvNumberInput from "@/components/common/pv-number-input.vue";
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
import { getResolutionString, resolutionsAreEqual } from "@/lib/PhotonUtils";
import CameraCalibrationInfoCard from "@/components/cameras/CameraCalibrationInfoCard.vue";
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
const settingsValid = ref(true);
@@ -74,6 +75,15 @@ const squareSizeIn = ref(1);
const patternWidth = ref(8);
const patternHeight = ref(8);
const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Chessboard);
const useMrCalRef = ref(true);
const useMrCal = computed<boolean>({
get() {
return useMrCalRef.value && useSettingsStore().general.mrCalWorking;
},
set(value) {
useMrCalRef.value = value && useSettingsStore().general.mrCalWorking;
}
});
const downloadCalibBoard = () => {
const doc = new JsPDF({ unit: "in", format: "letter" });
@@ -188,7 +198,8 @@ const startCalibration = () => {
squareSizeIn: squareSizeIn.value,
patternHeight: patternHeight.value,
patternWidth: patternWidth.value,
boardType: boardType.value
boardType: boardType.value,
useMrCal: useMrCal.value
});
// The Start PnP method already handles updating the backend so only a store update is required
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
@@ -314,6 +325,23 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
:label-cols="5"
/>
<pv-switch
v-model="useMrCal"
label="Try using MrCal over OpenCV"
:disabled="!useSettingsStore().general.mrCalWorking || isCalibrating"
tooltip="If enabled, Photon will (try to) use MrCal instead of OpenCV for camera calibration."
:label-cols="5"
/>
<v-banner
v-show="!useSettingsStore().general.mrCalWorking"
rounded
color="red"
text-color="white"
class="mt-3"
icon="mdi-alert-circle-outline"
>
MrCal JNI could not be loaded! Consult journalctl logs for additional details.
</v-banner>
</v-form>
<v-row justify="center">
<v-chip

View File

@@ -241,6 +241,22 @@ const getObservationDetails = (): ObservationDetails[] | undefined => {
<td>Diagonal FOV</td>
<td>{{ videoFormat.diagonalFOV !== undefined ? videoFormat.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
</tr>
<!-- Board warp, only shown for mrcal-calibrated cameras -->
<tr
v-if="
useCameraSettingsStore().getCalibrationCoeffs(props.videoFormat.resolution)?.calobjectWarp?.length === 2
"
>
<td>Board warp, X/Y</td>
<td>
{{
useCameraSettingsStore()
.getCalibrationCoeffs(props.videoFormat.resolution)
?.calobjectWarp?.map((it) => (it * 1000).toFixed(2) + " mm")
.join(" / ")
}}
</td>
</tr>
</tbody>
</template>
</v-simple-table>

View File

@@ -317,6 +317,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
patternWidth: number;
patternHeight: number;
boardType: CalibrationBoardTypes;
useMrCal: boolean;
},
cameraIndex: number = useStateStore().currentCameraIndex
) {

View File

@@ -26,7 +26,8 @@ export const useSettingsStore = defineStore("settings", {
version: undefined,
gpuAcceleration: undefined,
hardwareModel: undefined,
hardwarePlatform: undefined
hardwarePlatform: undefined,
mrCalWorking: true
},
network: {
ntServerAddress: "",
@@ -97,7 +98,8 @@ export const useSettingsStore = defineStore("settings", {
version: data.general.version || undefined,
hardwareModel: data.general.hardwareModel || undefined,
hardwarePlatform: data.general.hardwarePlatform || undefined,
gpuAcceleration: data.general.gpuAcceleration || undefined
gpuAcceleration: data.general.gpuAcceleration || undefined,
mrCalWorking: data.general.mrCalWorking
};
this.lighting = data.lighting;
this.network = data.networkSettings;

View File

@@ -6,6 +6,7 @@ export interface GeneralSettings {
gpuAcceleration?: string;
hardwareModel?: string;
hardwarePlatform?: string;
mrCalWorking: boolean;
}
export interface MetricData {
@@ -131,6 +132,7 @@ export interface CameraCalibrationResult {
cameraIntrinsics: JsonMatOfDouble;
distCoeffs: JsonMatOfDouble;
observations: BoardObservation[];
calobjectWarp?: number[];
}
export interface ConfigurableCameraSettings {

View File

@@ -16,6 +16,19 @@ dependencies {
implementation "org.photonvision:photon-libcamera-gl-driver-jni:$photonGlDriverLibVersion:linuxarm64"
implementation "org.photonvision:photon-libcamera-gl-driver-java:$photonGlDriverLibVersion"
implementation "org.photonvision:photon-mrcal-java:$mrcalVersion"
// Only include mrcal natives on platforms that we build for
if (!(jniPlatform in [
"osxx86-64",
"osxarm64",
"linuxarm32"
])) {
implementation "org.photonvision:photon-mrcal-jni:$mrcalVersion:$wpilibNativeName"
}
testImplementation group: 'org.junit-pioneer' , name: 'junit-pioneer', version: '2.2.0'
}
task writeCurrentVersion {

View File

@@ -27,6 +27,7 @@ import org.photonvision.PhotonVersion;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.common.util.SerializationUtils;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.processes.VisionModule;
@@ -140,6 +141,7 @@ public class PhotonConfiguration {
LibCameraJNILoader.isSupported()
? "Zerocopy Libcamera Working"
: ""); // TODO add support for other types of GPU accel
generalSubmap.put("mrCalWorking", MrCalJNILoader.isWorking());
generalSubmap.put("hardwareModel", hardwareConfig.deviceName);
generalSubmap.put("hardwarePlatform", Platform.getPlatformName());
settingsSubmap.put("general", generalSubmap);

View File

@@ -17,6 +17,7 @@
package org.photonvision.common.hardware;
import com.jogamp.common.os.Platform.OSType;
import edu.wpi.first.util.RuntimeDetector;
import java.io.BufferedReader;
import java.io.IOException;
@@ -27,23 +28,32 @@ import org.photonvision.common.util.ShellExec;
@SuppressWarnings("unused")
public enum Platform {
// WPILib Supported (JNI)
WINDOWS_64("Windows x64", false, OSType.WINDOWS, true),
LINUX_32("Linux x86", false, OSType.LINUX, true),
LINUX_64("Linux x64", false, OSType.LINUX, true),
WINDOWS_64("Windows x64", "winx64", false, OSType.WINDOWS, true),
LINUX_32("Linux x86", "linuxx64", false, OSType.LINUX, true),
LINUX_64("Linux x64", "linuxx64", false, OSType.LINUX, true),
LINUX_RASPBIAN32(
"Linux Raspbian 32-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 32-bit image
"Linux Raspbian 32-bit",
"linuxarm32",
true,
OSType.LINUX,
true), // Raspberry Pi 3/4 with a 32-bit image
LINUX_RASPBIAN64(
"Linux Raspbian 64-bit", true, OSType.LINUX, true), // Raspberry Pi 3/4 with a 64-bit image
LINUX_AARCH64("Linux AARCH64", false, OSType.LINUX, true), // Jetson Nano, Jetson TX2
"Linux Raspbian 64-bit",
"linuxarm64",
true,
OSType.LINUX,
true), // Raspberry Pi 3/4 with a 64-bit image
LINUX_AARCH64(
"Linux AARCH64", "linuxarm64", false, OSType.LINUX, true), // Jetson Nano, Jetson TX2
// PhotonVision Supported (Manual build/install)
LINUX_ARM32("Linux ARM32", false, OSType.LINUX, true), // ODROID XU4, C1+
LINUX_ARM64("Linux ARM64", false, OSType.LINUX, true), // ODROID C2, N2
LINUX_ARM32("Linux ARM32", "linuxarm32", false, OSType.LINUX, true), // ODROID XU4, C1+
LINUX_ARM64("Linux ARM64", "linuxarm64", false, OSType.LINUX, true), // ODROID C2, N2
// Completely unsupported
WINDOWS_32("Windows x86", false, OSType.WINDOWS, false),
MACOS("Mac OS", false, OSType.MACOS, false),
UNKNOWN("Unsupported Platform", false, OSType.UNKNOWN, false);
WINDOWS_32("Windows x86", "windowsx64", false, OSType.WINDOWS, false),
MACOS("Mac OS", "osxuniversal", false, OSType.MACOS, false),
UNKNOWN("Unsupported Platform", "", false, OSType.UNKNOWN, false);
private enum OSType {
WINDOWS,
@@ -54,6 +64,7 @@ public enum Platform {
private static final ShellExec shell = new ShellExec(true, false);
public final String description;
public final String nativeLibraryFolderName;
public final boolean isPi;
public final OSType osType;
public final boolean isSupported;
@@ -62,11 +73,17 @@ public enum Platform {
private static final Platform currentPlatform = getCurrentPlatform();
private static final boolean isRoot = checkForRoot();
Platform(String description, boolean isPi, OSType osType, boolean isSupported) {
Platform(
String description,
String nativeLibFolderName,
boolean isPi,
OSType osType,
boolean isSupported) {
this.description = description;
this.isPi = isPi;
this.osType = osType;
this.isSupported = isSupported;
this.nativeLibraryFolderName = nativeLibFolderName;
}
//////////////////////////////////////////////////////
@@ -89,6 +106,10 @@ public enum Platform {
}
}
public static String getNativeLibraryFolderName() {
return currentPlatform.nativeLibraryFolderName;
}
public static boolean isRoot() {
return isRoot;
}
@@ -214,4 +235,9 @@ public enum Platform {
return false;
}
}
public static boolean isWindows() {
var p = getCurrentPlatform();
return (p == WINDOWS_32 || p == WINDOWS_64);
}
}

View File

@@ -39,7 +39,11 @@ import org.opencv.highgui.HighGui;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
public class TestUtils {
private static boolean has_loaded = false;
public static boolean loadLibraries() {
if (has_loaded) return true;
NetworkTablesJNI.Helper.setExtractOnStaticLoad(false);
WPIUtilJNI.Helper.setExtractOnStaticLoad(false);
WPIMathJNI.Helper.setExtractOnStaticLoad(false);
@@ -61,11 +65,13 @@ public class TestUtils {
"cscorejni",
"apriltagjni");
return true;
has_loaded = true;
} catch (IOException e) {
e.printStackTrace();
return false;
has_loaded = false;
}
return has_loaded;
}
@SuppressWarnings("unused")

View File

@@ -0,0 +1,83 @@
/*
* 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.jni;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
public abstract class PhotonJNICommon {
static boolean libraryLoaded = false;
protected static Logger logger = null;
protected static synchronized void forceLoad(Class<?> clazz, List<String> libraries)
throws IOException {
if (libraryLoaded) return;
if (logger == null) logger = new Logger(clazz, LogGroup.Camera);
for (var libraryName : libraries) {
try {
// We always extract the shared object (we could hash each so, but that's a lot of work)
var arch_name = Platform.getNativeLibraryFolderName();
var nativeLibName = System.mapLibraryName(libraryName);
var in = clazz.getResourceAsStream("/nativelibraries/" + arch_name + "/" + nativeLibName);
if (in == null) {
libraryLoaded = false;
return;
}
// It's important that we don't mangle the names of these files on Windows at least
File temp = new File(System.getProperty("java.io.tmpdir"), nativeLibName);
FileOutputStream fos = new FileOutputStream(temp);
int read = -1;
byte[] buffer = new byte[1024];
while ((read = in.read(buffer)) != -1) {
fos.write(buffer, 0, read);
}
fos.close();
in.close();
System.load(temp.getAbsolutePath());
logger.info("Successfully loaded shared object " + temp.getName());
} catch (UnsatisfiedLinkError e) {
logger.error("Couldn't load shared object " + libraryName, e);
e.printStackTrace();
// logger.error(System.getProperty("java.library.path"));
break;
}
}
libraryLoaded = true;
}
protected static synchronized void forceLoad(Class<?> clazz, String libraryName)
throws IOException {
forceLoad(clazz, List.of(libraryName));
}
public static boolean isWorking() {
return libraryLoaded;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.mrcal;
import java.io.IOException;
import java.util.List;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.util.TestUtils;
import org.photonvision.jni.PhotonJNICommon;
public class MrCalJNILoader extends PhotonJNICommon {
public static synchronized void forceLoad() throws IOException {
// Force load opencv
TestUtils.loadLibraries();
// Library naming is dumb and has "lib" appended for Windows when it ought not to
if (Platform.isWindows()) {
// Order is correct to match dependencies of libraries
forceLoad(
MrCalJNILoader.class,
List.of(
"libamd",
"libcamd",
"libcolamd",
"libccolamd",
"openblas",
"libgcc_s_seh-1",
"libgfortran-5",
"liblapack",
"libcholmod",
"mrcal_jni"));
} else {
// Nothing else to do on linux
forceLoad(MrCalJNILoader.class, List.of("mrcal_jni"));
}
if (!MrCalJNILoader.isWorking()) {
throw new IOException("Unable to load mrcal JNI!");
}
}
}

View File

@@ -23,6 +23,10 @@ import java.io.IOException;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
/**
* Helper for extracting photon-libcamera-gl-driver shared library files. TODO: Refactor to use
* PhotonJNICommon
*/
public class LibCameraJNILoader {
private static boolean libraryLoaded = false;
private static final Logger logger = new Logger(LibCameraJNILoader.class, LogGroup.Camera);

View File

@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Arrays;
import java.util.List;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDouble;
@@ -47,6 +48,12 @@ public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("calobjectWarp")
public final double[] calobjectWarp;
@JsonProperty("calobjectSize")
public final Size calobjectSize;
@JsonProperty("calobjectSpacing")
public final double calobjectSpacing;
@JsonIgnore private final double[] intrinsicsArr = new double[9];
@JsonIgnore private final double[] distCoeffsArr = new double[5];
@@ -64,6 +71,9 @@ public class CameraCalibrationCoefficients implements Releasable {
* @param calobjectWarp Board deformation parameters, for calibrators that can estimate that. See:
* https://mrcal.secretsauce.net/formulation.html#board-deformation
* @param observations List of snapshots used to construct this calibration
* @param calobjectSize Dimensions of the object used to calibrate, in # of squares in
* width/height
* @param calobjectSpacing Spacing between adjacent squares, in meters
*/
@JsonCreator
public CameraCalibrationCoefficients(
@@ -71,11 +81,15 @@ public class CameraCalibrationCoefficients implements Releasable {
@JsonProperty("cameraIntrinsics") JsonMatOfDouble cameraIntrinsics,
@JsonProperty("distCoeffs") JsonMatOfDouble distCoeffs,
@JsonProperty("calobjectWarp") double[] calobjectWarp,
@JsonProperty("observations") List<BoardObservation> observations) {
@JsonProperty("observations") List<BoardObservation> observations,
@JsonProperty("calobjectSize") Size calobjectSize,
@JsonProperty("calobjectSpacing") double calobjectSpacing) {
this.resolution = resolution;
this.cameraIntrinsics = cameraIntrinsics;
this.distCoeffs = distCoeffs;
this.calobjectWarp = calobjectWarp;
this.calobjectSize = calobjectSize;
this.calobjectSpacing = calobjectSpacing;
// Legacy migration just to make sure that observations is at worst empty and never null
if (observations == null) {
@@ -150,11 +164,35 @@ public class CameraCalibrationCoefficients implements Releasable {
var cam_jsonmat = new JsonMatOfDouble(3, 3, cam_arr);
var distortion_jsonmat = new JsonMatOfDouble(1, 5, dist_array);
var error = json.get("avg_reprojection_error").asDouble();
var width = json.get("img_size").get(0).doubleValue();
var height = json.get("img_size").get(1).doubleValue();
return new CameraCalibrationCoefficients(
new Size(width, height), cam_jsonmat, distortion_jsonmat, new double[0], List.of());
new Size(width, height),
cam_jsonmat,
distortion_jsonmat,
new double[0],
List.of(),
new Size(0, 0),
0);
}
@Override
public String toString() {
return "CameraCalibrationCoefficients [resolution="
+ resolution
+ ", cameraIntrinsics="
+ cameraIntrinsics
+ ", distCoeffs="
+ distCoeffs
+ ", observations="
+ observations
+ ", calobjectWarp="
+ Arrays.toString(calobjectWarp)
+ ", intrinsicsArr="
+ Arrays.toString(intrinsicsArr)
+ ", distCoeffsArr="
+ Arrays.toString(distCoeffsArr)
+ "]";
}
}

View File

@@ -127,4 +127,23 @@ public class JsonMatOfDouble implements Releasable {
packet.encode(this.data);
return packet;
}
@Override
public String toString() {
return "JsonMat [rows="
+ rows
+ ", cols="
+ cols
+ ", type="
+ type
+ ", data="
+ Arrays.toString(data)
+ ", wrappedMat="
+ wrappedMat
+ ", wpilibMat="
+ wpilibMat
+ ", wrappedMatOfDouble="
+ wrappedMatOfDouble
+ "]";
}
}

View File

@@ -17,19 +17,22 @@
package org.photonvision.vision.pipe.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.*;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDouble;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.math.MathUtils;
import org.photonvision.mrcal.MrCalJNI;
import org.photonvision.mrcal.MrCalJNI.MrCalResult;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.BoardObservation;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.calibration.JsonImageMat;
@@ -41,19 +44,9 @@ public class Calibrate3dPipe
List<FindBoardCornersPipe.FindBoardCornersPipeResult>,
CameraCalibrationCoefficients,
Calibrate3dPipe.CalibratePipeParams> {
// Camera matrix stores the center of the image and focal length across the x and y-axis in a 3x3
// matrix
private final Mat cameraMatrix = new Mat();
// Stores the radical and tangential distortion in a 5x1 matrix
private final MatOfDouble distortionCoefficients = new MatOfDouble();
// For logging
private static final Logger logger = new Logger(Calibrate3dPipe.class, LogGroup.General);
// Translational and rotational matrices
private final List<Mat> rvecs = new ArrayList<>();
private final List<Mat> tvecs = new ArrayList<>();
// The Standard deviation of the estimated parameters
private final Mat stdDeviationsIntrinsics = new Mat();
private final Mat stdDeviationsExtrinsics = new Mat();
@@ -62,9 +55,6 @@ public class Calibrate3dPipe
// finding the Euclidean distance between the actual corners.
private final Mat perViewErrors = new Mat();
// RMS of the calibration
private double calibrationAccuracy;
/**
* Runs the process for the pipe.
*
@@ -84,6 +74,35 @@ public class Calibrate3dPipe
&& it.size != null)
.collect(Collectors.toList());
CameraCalibrationCoefficients ret;
var start = System.nanoTime();
if (MrCalJNILoader.isWorking() && params.useMrCal) {
logger.debug("Calibrating with mrcal!");
ret = calibrateMrcal(in);
} else {
logger.debug("Calibrating with opencv!");
ret = calibrateOpenCV(in);
}
var dt = System.nanoTime() - start;
if (ret != null)
logger.info(
"CALIBRATION SUCCESS for res "
+ in.get(0).size
+ " in "
+ dt / 1e6
+ "ms! camMatrix: \n"
+ Arrays.toString(ret.cameraIntrinsics.data)
+ "\ndistortionCoeffs:\n"
+ Arrays.toString(ret.distCoeffs.data)
+ "\n");
else logger.info("Calibration failed! Review log for more details");
return ret;
}
protected CameraCalibrationCoefficients calibrateOpenCV(
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
List<Mat> objPoints = in.stream().map(it -> it.objectPoints).collect(Collectors.toList());
List<Mat> imgPts = in.stream().map(it -> it.imagePoints).collect(Collectors.toList());
if (objPoints.size() != imgPts.size()) {
@@ -91,6 +110,14 @@ public class Calibrate3dPipe
return null;
}
Mat cameraMatrix = new Mat();
MatOfDouble distortionCoefficients = new MatOfDouble();
List<Mat> rvecs = new ArrayList<>();
List<Mat> tvecs = new ArrayList<>();
// RMS of the calibration
double calibrationAccuracy;
try {
// FindBoardCorners pipe outputs all the image points, object points, and frames to calculate
// imageSize from, other parameters are output Mats
@@ -116,6 +143,116 @@ public class Calibrate3dPipe
JsonMatOfDouble cameraMatrixMat = JsonMatOfDouble.fromMat(cameraMatrix);
JsonMatOfDouble distortionCoefficientsMat = JsonMatOfDouble.fromMat(distortionCoefficients);
var observations =
createObservations(in, cameraMatrix, distortionCoefficients, rvecs, tvecs, null);
cameraMatrix.release();
distortionCoefficients.release();
rvecs.forEach(Mat::release);
tvecs.forEach(Mat::release);
return new CameraCalibrationCoefficients(
in.get(0).size,
cameraMatrixMat,
distortionCoefficientsMat,
new double[0],
observations,
new Size(params.boardWidth, params.boardHeight),
params.squareSize);
}
protected CameraCalibrationCoefficients calibrateMrcal(
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in) {
List<MatOfPoint2f> corner_locations =
in.stream().map(it -> it.imagePoints).map(MatOfPoint2f::new).collect(Collectors.toList());
int imageWidth = (int) in.get(0).size.width;
int imageHeight = (int) in.get(0).size.height;
final double FOCAL_LENGTH_GUESS = 1200;
MrCalResult result =
MrCalJNI.calibrateCamera(
corner_locations,
params.boardWidth,
params.boardHeight,
params.squareSize,
imageWidth,
imageHeight,
FOCAL_LENGTH_GUESS);
// intrinsics are fx fy cx cy from mrcal
JsonMatOfDouble cameraMatrixMat =
new JsonMatOfDouble(
3,
3,
CvType.CV_64FC1,
new double[] {
// fx 0 cx
result.intrinsics[0],
0,
result.intrinsics[2],
// 0 fy cy
0,
result.intrinsics[1],
result.intrinsics[3],
// 0 0 1
0,
0,
1
});
JsonMatOfDouble distortionCoefficientsMat =
new JsonMatOfDouble(1, 8, CvType.CV_64FC1, Arrays.copyOfRange(result.intrinsics, 4, 12));
// Calculate optimized board poses manually. We get this for free from mrcal too, but that's not
// JNIed (yet)
List<Mat> rvecs = new ArrayList<>();
List<Mat> tvecs = new ArrayList<>();
for (var o : in) {
var rvec = new Mat();
var tvec = new Mat();
Calib3d.solvePnP(
o.objectPoints,
o.imagePoints,
cameraMatrixMat.getAsMat(),
distortionCoefficientsMat.getAsMatOfDouble(),
rvec,
tvec);
rvecs.add(rvec);
tvecs.add(tvec);
}
List<BoardObservation> observations =
createObservations(
in,
cameraMatrixMat.getAsMat(),
distortionCoefficientsMat.getAsMatOfDouble(),
rvecs,
tvecs,
new double[] {result.warp_x, result.warp_y});
rvecs.forEach(Mat::release);
tvecs.forEach(Mat::release);
return new CameraCalibrationCoefficients(
in.get(0).size,
cameraMatrixMat,
distortionCoefficientsMat,
new double[] {result.warp_x, result.warp_y},
observations,
new Size(params.boardWidth, params.boardHeight),
params.squareSize);
}
private List<BoardObservation> createObservations(
List<FindBoardCornersPipe.FindBoardCornersPipeResult> in,
Mat cameraMatrix_,
MatOfDouble distortionCoefficients_,
List<Mat> rvecs,
List<Mat> tvecs,
double[] calobject_warp) {
List<Mat> objPoints = in.stream().map(it -> it.objectPoints).collect(Collectors.toList());
List<Mat> imgPts = in.stream().map(it -> it.imagePoints).collect(Collectors.toList());
// For each observation, calc reprojection error
Mat jac_temp = new Mat();
List<BoardObservation> observations = new ArrayList<>();
@@ -125,14 +262,36 @@ public class Calibrate3dPipe
var i_objPts = i_objPtsNative.toList();
var i_imgPts = ((MatOfPoint2f) imgPts.get(i)).toList();
// Apply warp, if set
if (calobject_warp != null && calobject_warp.length == 2) {
// mrcal warp model!
// The chessboard spans [-1, 1] on the x and y axies. We then let z=k_x(1-x^2)+k_y(1-y^2)
double xmin = 0;
double ymin = 0;
double xmax = params.boardWidth * params.squareSize;
double ymax = params.boardHeight * params.squareSize;
double k_x = calobject_warp[0];
double k_y = calobject_warp[1];
// Convert to list, remap z, and back to cv::Mat
var list = i_objPtsNative.toArray();
for (var pt : list) {
double x_norm = MathUtils.map(pt.x, xmin, xmax, -1, 1);
double y_norm = MathUtils.map(pt.y, ymin, ymax, -1, 1);
pt.z = k_x * (1 - x_norm * x_norm) + k_y * (1 - y_norm * y_norm);
}
i_objPtsNative.fromArray(list);
}
var img_pts_reprojected = new MatOfPoint2f();
try {
Calib3d.projectPoints(
i_objPtsNative,
rvecs.get(i),
tvecs.get(i),
cameraMatrix,
distortionCoefficients,
cameraMatrix_,
distortionCoefficients_,
img_pts_reprojected,
jac_temp,
0.0);
@@ -164,33 +323,24 @@ public class Calibrate3dPipe
}
jac_temp.release();
// Standard deviation of results
try {
// Print calibration successful
logger.info(
"CALIBRATION SUCCESS for res "
+ params.resolution
+ " (with accuracy "
+ calibrationAccuracy
+ ")! camMatrix: \n"
+ new ObjectMapper().writeValueAsString(cameraMatrixMat)
+ "\ndistortionCoeffs:\n"
+ new ObjectMapper().writeValueAsString(distortionCoefficientsMat)
+ "\n");
} catch (JsonProcessingException e) {
logger.error("Failed to parse calibration data to json!", e);
}
return new CameraCalibrationCoefficients(
params.resolution, cameraMatrixMat, distortionCoefficientsMat, new double[0], observations);
return observations;
}
public static class CalibratePipeParams {
// Only needs resolution to pass onto CameraCalibrationCoefficients object.
private final Size resolution;
// Size (in # of corners) of the calibration object
public int boardHeight;
public int boardWidth;
// And size of each square
public double squareSize;
public CalibratePipeParams(Size resolution) {
// logger.info("res: " + resolution.toString());
this.resolution = resolution;
public boolean useMrCal;
public CalibratePipeParams(
int boardHeightSquares, int boardWidthSquares, double squareSize, boolean usemrcal) {
this.boardHeight = boardHeightSquares - 1;
this.boardWidth = boardWidthSquares - 1;
this.squareSize = squareSize;
this.useMrCal = usemrcal;
}
}
}

View File

@@ -261,7 +261,7 @@ public class FindBoardCornersPipe
var outBoardCorners = new MatOfPoint2f();
boardCorners.copyTo(outBoardCorners);
var objPts = new MatOfPoint2f();
var objPts = new MatOfPoint3f();
objectPoints.copyTo(objPts);
// Get the size of the inFrame
@@ -329,14 +329,14 @@ public class FindBoardCornersPipe
public static class FindBoardCornersPipeResult implements Releasable {
public Size size;
public MatOfPoint2f objectPoints;
public MatOfPoint3f objectPoints;
public MatOfPoint2f imagePoints;
// Set later only if we need it
public Mat inputImage = null;
public FindBoardCornersPipeResult(
Size size, MatOfPoint2f objectPoints, MatOfPoint2f imagePoints) {
Size size, MatOfPoint3f objectPoints, MatOfPoint2f imagePoints) {
this.size = size;
this.objectPoints = objectPoints;
this.imagePoints = imagePoints;

View File

@@ -24,7 +24,6 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Size;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.logging.LogGroup;
@@ -58,7 +57,7 @@ public class Calibrate3dPipeline
private boolean takeSnapshot = false;
// Output of the corners
final List<FindBoardCornersPipeResult> foundCornersList;
public final List<FindBoardCornersPipeResult> foundCornersList;
/// Output of the calibration, getter method is set for this.
private CVPipeResult<CameraCalibrationCoefficients> calibrationOutput;
@@ -93,7 +92,7 @@ public class Calibrate3dPipeline
Calibrate3dPipe.CalibratePipeParams calibratePipeParams =
new Calibrate3dPipe.CalibratePipeParams(
new Size(frameStaticProperties.imageWidth, frameStaticProperties.imageHeight));
settings.boardHeight, settings.boardWidth, settings.gridSize, settings.useMrCal);
calibrate3dPipe.setParams(calibratePipeParams);
}
@@ -210,7 +209,8 @@ public class Calibrate3dPipeline
Units.metersToInches(settings.gridSize),
settings.boardWidth,
settings.boardHeight,
settings.boardType));
settings.boardType,
settings.useMrCal));
DataChangeService.getInstance()
.publishEvent(OutgoingUIEvent.wrappedOf("calibrationData", state));

View File

@@ -28,6 +28,7 @@ public class Calibration3dPipelineSettings extends AdvancedPipelineSettings {
public double gridSize = Units.inchesToMeters(1.0);
public Size resolution = new Size(640, 480);
public boolean useMrCal = true;
public Calibration3dPipelineSettings() {
super();

View File

@@ -26,6 +26,7 @@ public class UICalibrationData {
public int patternWidth;
public int patternHeight;
public BoardType boardType;
public boolean useMrCal;
public UICalibrationData() {}
@@ -37,7 +38,8 @@ public class UICalibrationData {
double squareSizeIn,
int patternWidth,
int patternHeight,
BoardType boardType) {
BoardType boardType,
boolean useMrCal) {
this.count = count;
this.minCount = minCount;
this.videoModeIndex = videoModeIndex;
@@ -46,6 +48,7 @@ public class UICalibrationData {
this.patternWidth = patternWidth;
this.patternHeight = patternHeight;
this.boardType = boardType;
this.useMrCal = useMrCal;
}
public enum BoardType {

View File

@@ -351,6 +351,7 @@ public class VisionModule {
settings.boardHeight = data.patternHeight;
settings.boardWidth = data.patternWidth;
settings.boardType = data.boardType;
settings.useMrCal = data.useMrCal;
settings.resolution = resolution;
// Disable gain if not applicable

View File

@@ -22,17 +22,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import edu.wpi.first.math.util.Units;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.junitpioneer.jupiter.cartesian.CartesianTest.Enum;
import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.TestUtils;
import org.photonvision.mrcal.MrCalJNILoader;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.QuirkyCamera;
import org.photonvision.vision.frame.Frame;
@@ -40,226 +44,104 @@ import org.photonvision.vision.frame.FrameDivisor;
import org.photonvision.vision.frame.FrameStaticProperties;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.CVMat;
import org.photonvision.vision.pipe.impl.Calibrate3dPipe;
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe;
import org.photonvision.vision.pipe.impl.FindBoardCornersPipe.FindBoardCornersPipeResult;
public class Calibrate3dPipeTest {
@BeforeAll
public static void init() {
public static void init() throws IOException {
TestUtils.loadLibraries();
MrCalJNILoader.forceLoad();
var logLevel = LogLevel.DEBUG;
Logger.setLevel(LogGroup.Camera, logLevel);
Logger.setLevel(LogGroup.WebServer, logLevel);
Logger.setLevel(LogGroup.VisionModule, logLevel);
Logger.setLevel(LogGroup.Data, logLevel);
Logger.setLevel(LogGroup.Config, logLevel);
Logger.setLevel(LogGroup.General, logLevel);
}
@Test
public void perViewErrorsTest() {
List<Mat> frames = new ArrayList<>();
enum CalibrationDatasets {
LIFECAM_480("lifecam/2024-01-02_lifecam_480", new Size(640, 480), new Size(11, 11)),
LIFECAM_1280("lifecam/2024-01-02_lifecam_1280", new Size(1280, 720), new Size(11, 11));
File dir = new File(TestUtils.getDotBoardImagesPath().toAbsolutePath().toString());
File[] directoryListing = dir.listFiles();
for (var file : directoryListing) {
frames.add(Imgcodecs.imread(file.getAbsolutePath()));
}
final String path;
final Size size;
final Size boardSize;
FindBoardCornersPipe findBoardCornersPipe = new FindBoardCornersPipe();
findBoardCornersPipe.setParams(
new FindBoardCornersPipe.FindCornersPipeParams(
11, 4, UICalibrationData.BoardType.DOTBOARD, 15, FrameDivisor.NONE));
List<FindBoardCornersPipeResult> foundCornersList = new ArrayList<>();
for (var f : frames) {
var copy = new Mat();
f.copyTo(copy);
foundCornersList.add(findBoardCornersPipe.run(Pair.of(f, copy)).output);
}
Calibrate3dPipe calibrate3dPipe = new Calibrate3dPipe();
calibrate3dPipe.setParams(new Calibrate3dPipe.CalibratePipeParams(new Size(640, 480)));
var calibrate3dPipeOutput = calibrate3dPipe.run(foundCornersList);
assertTrue(calibrate3dPipeOutput.output.observations.size() > 0);
for (var f : frames) {
f.release();
private CalibrationDatasets(String path, Size image, Size chessboard) {
this.path = path;
this.size = image;
this.boardSize = chessboard;
}
}
@Test
public void calibrationPipelineTest() {
int startMatCount = CVMat.getMatCount();
File dir = new File(TestUtils.getDotBoardImagesPath().toAbsolutePath().toString());
File[] directoryListing = dir.listFiles();
Calibrate3dPipeline calibration3dPipeline = new Calibrate3dPipeline(20, "unique_name_lol");
calibration3dPipeline.getSettings().boardHeight = 11;
calibration3dPipeline.getSettings().boardWidth = 4;
calibration3dPipeline.getSettings().boardType = UICalibrationData.BoardType.DOTBOARD;
calibration3dPipeline.getSettings().gridSize = 15;
calibration3dPipeline.getSettings().resolution = new Size(640, 480);
for (var file : directoryListing) {
calibration3dPipeline.takeSnapshot();
var frame =
new Frame(
new CVMat(Imgcodecs.imread(file.getAbsolutePath())),
new CVMat(),
FrameThresholdType.NONE,
new FrameStaticProperties(640, 480, 60, null));
var output = calibration3dPipeline.run(frame, QuirkyCamera.DefaultCamera);
// TestUtils.showImage(output.inputAndOutputFrame.processedImage.getMat());
output.release();
frame.release();
}
assertTrue(
calibration3dPipeline.foundCornersList.stream()
.map(it -> it.imagePoints)
.allMatch(it -> it.width() > 0 && it.height() > 0));
calibration3dPipeline.removeSnapshot(0);
var frame =
new Frame(
new CVMat(Imgcodecs.imread(directoryListing[0].getAbsolutePath())),
new CVMat(),
FrameThresholdType.NONE,
new FrameStaticProperties(640, 480, 60, null));
calibration3dPipeline.run(frame, QuirkyCamera.DefaultCamera).release();
frame.release();
assertTrue(
calibration3dPipeline.foundCornersList.stream()
.map(it -> it.imagePoints)
.allMatch(it -> it.width() > 0 && it.height() > 0));
var cal = calibration3dPipeline.tryCalibration();
calibration3dPipeline.finishCalibration();
assertNotNull(cal);
assertNotNull(cal.observations);
System.out.println("Camera Intrinsics: " + cal.cameraIntrinsics.toString());
System.out.println("Dist Coeffs: " + cal.distCoeffs.toString());
// Confirm we didn't get leaky on our mat usage
// assertTrue(CVMat.getMatCount() == startMatCount); // TODO Figure out why this doesn't work in
// CI
System.out.println("CVMats left: " + CVMat.getMatCount() + " Start: " + startMatCount);
}
@Test
public void calibrateSquares320x240_pi() {
/**
* Run camera calibration on a given dataset
*
* @param dataset Location of images and their size
* @param useMrCal If we should use mrcal or opencv for camera calibration
*/
@CartesianTest
public void calibrateTestMatrix(
@Enum(CalibrationDatasets.class) CalibrationDatasets dataset,
@Values(booleans = {true, false}) boolean useMrCal) {
// Pi3 and V1.3 camera
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "piCam", "320_240_1").toFile();
Size sz = new Size(320, 240);
calibrateSquaresCommon(sz, dir);
File dir = Path.of(base, dataset.path).toFile();
calibrateSquaresCommon(dataset.size, dir, dataset.boardSize, useMrCal);
}
@Test
public void calibrateSquares640x480_pi() {
// Pi3 and V1.3 camera
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "piCam", "640_480_1").toFile();
Size sz = new Size(640, 480);
calibrateSquaresCommon(sz, dir);
}
@Test
public void calibrateSquares960x720_pi() {
// Pi3 and V1.3 camera
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "piCam", "960_720_1").toFile();
Size sz = new Size(960, 720);
calibrateSquaresCommon(sz, dir);
}
@Test
public void calibrateSquares1920x1080_pi() {
// Pi3 and V1.3 camera
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "piCam", "1920_1080_1").toFile();
Size sz = new Size(1920, 1080);
calibrateSquaresCommon(sz, dir);
}
@Test
public void calibrateSquares320x240_gloworm() {
// Gloworm Beta
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "gloworm", "320_240_1").toFile();
Size sz = new Size(320, 240);
Size boardDim = new Size(9, 7);
calibrateSquaresCommon(sz, dir, boardDim);
}
@Test
public void calibrateSquares_960_720_gloworm() {
// Gloworm Beta
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "gloworm", "960_720_1").toFile();
Size sz = new Size(960, 720);
Size boardDim = new Size(9, 7);
calibrateSquaresCommon(sz, dir, boardDim);
}
@Test
public void calibrateSquares_1280_720_gloworm() {
// Gloworm Beta
// This image set will return a fairly offset Y-pixel for the optical center point
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "gloworm", "1280_720_1").toFile();
Size sz = new Size(1280, 720);
Size boardDim = new Size(9, 7);
calibrateSquaresCommon(sz, dir, boardDim, 640, 192);
}
@Test
public void calibrateSquares_1920_1080_gloworm() {
// Gloworm Beta
// This image set has most samples on the right, and is expected to return a slightly
// wonky calibration.
String base = TestUtils.getSquaresBoardImagesPath().toAbsolutePath().toString();
File dir = Path.of(base, "gloworm", "1920_1080_1").toFile();
Size sz = new Size(1920, 1080);
Size boardDim = new Size(9, 7);
calibrateSquaresCommon(sz, dir, boardDim, 1311, 540);
}
public void calibrateSquaresCommon(Size imgRes, File rootFolder) {
calibrateSquaresCommon(imgRes, rootFolder, new Size(8, 8));
}
public void calibrateSquaresCommon(Size imgRes, File rootFolder, Size boardDim) {
public static void calibrateSquaresCommon(
Size imgRes, File rootFolder, Size boardDim, boolean useMrCal) {
calibrateSquaresCommon(
imgRes, rootFolder, boardDim, Units.inchesToMeters(1), imgRes.width / 2, imgRes.height / 2);
imgRes,
rootFolder,
boardDim,
Units.inchesToMeters(1),
imgRes.width / 2,
imgRes.height / 2,
useMrCal);
}
public void calibrateSquaresCommon(
Size imgRes, File rootFolder, Size boardDim, double expectedXCenter, double expectedYCenter) {
public static void calibrateSquaresCommon(
Size imgRes,
File rootFolder,
Size boardDim,
double expectedXCenter,
double expectedYCenter,
boolean useMrCal) {
calibrateSquaresCommon(
imgRes, rootFolder, boardDim, Units.inchesToMeters(1), expectedXCenter, expectedYCenter);
imgRes,
rootFolder,
boardDim,
Units.inchesToMeters(1),
expectedXCenter,
expectedYCenter,
useMrCal);
}
public void calibrateSquaresCommon(
public static void calibrateSquaresCommon(
Size imgRes,
File rootFolder,
Size boardDim,
double boardGridSize_m,
double expectedXCenter,
double expectedYCenter) {
double expectedYCenter,
boolean useMrCal) {
int startMatCount = CVMat.getMatCount();
File[] directoryListing = rootFolder.listFiles();
assertTrue(directoryListing.length >= 25);
assertTrue(directoryListing.length >= 12);
Calibrate3dPipeline calibration3dPipeline = new Calibrate3dPipeline(20, "test_squares_common");
Calibrate3dPipeline calibration3dPipeline = new Calibrate3dPipeline(10, "test_squares_common");
calibration3dPipeline.getSettings().boardType = UICalibrationData.BoardType.CHESSBOARD;
calibration3dPipeline.getSettings().resolution = imgRes;
calibration3dPipeline.getSettings().boardHeight = (int) Math.round(boardDim.height);
calibration3dPipeline.getSettings().boardWidth = (int) Math.round(boardDim.width);
calibration3dPipeline.getSettings().gridSize = boardGridSize_m;
calibration3dPipeline.getSettings().streamingFrameDivisor = FrameDivisor.NONE;
calibration3dPipeline.getSettings().useMrCal = useMrCal;
for (var file : directoryListing) {
if (file.isFile()) {

View File

@@ -37,6 +37,7 @@ 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.mrcal.MrCalJNILoader;
import org.photonvision.raspi.LibCameraJNILoader;
import org.photonvision.server.Server;
import org.photonvision.vision.camera.FileVisionSource;
@@ -317,6 +318,14 @@ public class Main {
logger.error("Failed to load libcamera-JNI!", e);
}
try {
MrCalJNILoader.forceLoad();
} catch (IOException e) {
logger.warn(
"Failed to load mrcal-JNI! Camera calibration will fall back to opencv\n"
+ e.getMessage());
}
try {
if (!handleArgs(args)) {
System.exit(0);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -0,0 +1,411 @@
## generated with mrgingham --jobs 4 --gridn 7 img10.jpg img11.jpg img12.jpg img13.jpg img14.jpg img15.jpg img16.jpg img17.jpg img18.jpg img19.jpg img1.jpg img20.jpg img21.jpg img22.jpg img23.jpg img24.jpg img25.jpg img2.jpg img3.jpg img4.jpg img5.jpg img6.jpg img7.jpg img8.jpg img9.jpg
# filename x y level
img10.jpg - - -
img11.jpg - - -
img15.jpg 305.709000 174.707000 0
img15.jpg 351.673000 176.160000 0
img15.jpg 397.419000 177.562000 0
img15.jpg 442.075000 179.037000 0
img15.jpg 487.177000 180.891000 0
img15.jpg 531.785000 181.860000 0
img15.jpg 573.738000 183.557000 0
img15.jpg 304.294000 219.620000 0
img15.jpg 350.203000 220.724000 0
img15.jpg 395.748000 221.699000 0
img15.jpg 440.862000 222.973000 0
img15.jpg 485.520000 224.850000 0
img15.jpg 530.185000 225.869000 0
img15.jpg 572.114000 227.503000 0
img15.jpg 303.243000 263.590000 0
img15.jpg 349.341000 265.627000 0
img15.jpg 394.469000 266.043000 0
img15.jpg 439.742000 267.237000 0
img15.jpg 484.055000 268.790000 0
img15.jpg 528.175000 269.724000 0
img15.jpg 570.690000 270.726000 0
img15.jpg 301.669000 309.033000 0
img15.jpg 347.288000 309.528000 0
img15.jpg 393.567000 310.660000 0
img15.jpg 437.619000 311.441000 0
img15.jpg 482.058000 312.254000 0
img15.jpg 526.403000 313.246000 0
img15.jpg 569.039000 313.931000 0
img15.jpg 299.327000 353.836000 0
img15.jpg 345.584000 354.487000 0
img15.jpg 391.137000 354.882000 0
img15.jpg 436.249000 355.728000 0
img15.jpg 480.324000 356.082000 0
img15.jpg 524.946000 356.456000 0
img15.jpg 566.890000 357.050000 0
img15.jpg 297.979000 399.116000 0
img15.jpg 344.187000 399.653000 0
img15.jpg 389.909000 399.152000 0
img15.jpg 434.862000 399.209000 0
img15.jpg 478.911000 400.062000 0
img15.jpg 522.668000 399.882000 0
img15.jpg 565.371000 400.272000 0
img15.jpg 296.078000 445.016000 0
img15.jpg 342.710000 444.040000 0
img15.jpg 387.822000 443.536000 0
img15.jpg 433.286000 443.428000 0
img15.jpg 476.779000 442.870000 0
img15.jpg 520.055000 442.343000 0
img15.jpg 562.414000 442.205000 0
img14.jpg - - -
img18.jpg 91.257764 62.341333 0
img18.jpg 156.367723 66.974450 0
img18.jpg 218.066065 71.650665 0
img18.jpg 276.386861 76.251825 0
img18.jpg 331.055492 81.147211 0
img18.jpg 383.696897 84.814439 0
img18.jpg 430.893194 89.012836 0
img18.jpg 91.833674 123.430732 0
img18.jpg 155.905789 126.344950 0
img18.jpg 217.913026 129.702873 0
img18.jpg 274.982180 133.319740 0
img18.jpg 329.372274 135.975815 0
img18.jpg 380.871511 138.540811 0
img18.jpg 427.956504 141.086789 0
img18.jpg 91.771236 183.897303 0
img18.jpg 156.005710 185.474423 0
img18.jpg 217.247203 187.258936 0
img18.jpg 274.219614 188.919293 0
img18.jpg 327.751591 189.691818 0
img18.jpg 378.443874 191.387865 0
img18.jpg 425.847568 191.850230 0
img18.jpg 91.861943 243.611033 0
img18.jpg 155.182405 243.511549 0
img18.jpg 216.832614 243.122519 0
img18.jpg 273.129283 242.355705 0
img18.jpg 325.343307 241.717585 0
img18.jpg 375.851167 241.553501 0
img18.jpg 423.055064 241.803709 0
img18.jpg 91.671178 302.440746 0
img18.jpg 155.273091 300.177818 0
img18.jpg 215.216509 297.399528 0
img18.jpg 272.414663 294.579327 0
img18.jpg 323.101889 292.983598 0
img18.jpg 373.559284 291.323639 0
img18.jpg 419.835057 290.277082 0
img18.jpg 92.857058 359.214116 0
img18.jpg 154.937554 355.849957 0
img18.jpg 213.863967 351.613097 0
img18.jpg 269.476977 347.721722 0
img18.jpg 321.803464 344.059031 0
img18.jpg 371.343700 341.393939 0
img18.jpg 417.516845 338.833116 0
img18.jpg 93.077960 415.613843 0
img18.jpg 154.037428 409.923307 0
img18.jpg 212.834834 404.066145 0
img18.jpg 267.771666 398.702590 0
img18.jpg 319.298246 393.980064 0
img18.jpg 368.228850 389.593709 0
img18.jpg 414.674171 385.356734 0
img12.jpg - - -
img13.jpg 207.359000 161.061000 0
img13.jpg 256.830000 163.237000 0
img13.jpg 304.053000 165.752000 0
img13.jpg 349.537000 168.300000 0
img13.jpg 393.125000 170.923000 0
img13.jpg 436.193000 172.818000 0
img13.jpg 476.734000 174.922000 0
img13.jpg 206.200000 207.683000 0
img13.jpg 255.307000 209.547000 0
img13.jpg 303.050000 211.483000 0
img13.jpg 347.176000 213.290000 0
img13.jpg 391.548000 214.998000 0
img13.jpg 434.194000 216.182000 0
img13.jpg 475.306000 217.711000 0
img13.jpg 204.869000 254.591000 0
img13.jpg 253.717000 255.146000 0
img13.jpg 301.636000 256.939000 0
img13.jpg 346.212000 257.436000 0
img13.jpg 389.826000 258.667000 0
img13.jpg 432.929000 259.004000 0
img13.jpg 473.420000 260.297000 0
img13.jpg 203.314000 301.767000 0
img13.jpg 251.833000 301.487000 0
img13.jpg 299.666000 301.357000 0
img13.jpg 344.634000 301.545000 0
img13.jpg 387.881000 301.493000 0
img13.jpg 431.046000 302.380000 0
img13.jpg 471.777000 302.712000 0
img13.jpg 201.107000 348.792000 0
img13.jpg 249.800000 347.677000 0
img13.jpg 297.241000 347.004000 0
img13.jpg 343.254000 346.381000 0
img13.jpg 386.326000 345.487000 0
img13.jpg 429.810000 345.230000 0
img13.jpg 469.742000 345.034000 0
img13.jpg 199.756000 395.295000 0
img13.jpg 248.198000 394.029000 0
img13.jpg 295.721000 392.398000 0
img13.jpg 340.746000 390.831000 0
img13.jpg 384.770000 389.311000 0
img13.jpg 427.527000 388.627000 0
img13.jpg 468.236000 387.648000 0
img13.jpg 197.684000 442.702000 0
img13.jpg 246.477000 439.342000 0
img13.jpg 293.202000 437.257000 0
img13.jpg 339.300000 435.403000 0
img13.jpg 382.577000 432.917000 0
img13.jpg 425.605000 431.302000 0
img13.jpg 465.707000 429.225000 0
img19.jpg - - -
img21.jpg 171.432587 62.910910 0
img21.jpg 233.543966 66.989676 0
img21.jpg 292.892198 71.025168 0
img21.jpg 349.449118 75.488547 0
img21.jpg 402.475665 78.823783 0
img21.jpg 454.664044 82.293706 0
img21.jpg 502.641518 85.776245 0
img21.jpg 171.514734 121.993103 0
img21.jpg 232.390154 125.308964 0
img21.jpg 291.615192 128.775042 0
img21.jpg 347.095808 131.455464 0
img21.jpg 399.571916 133.920527 0
img21.jpg 451.377575 136.702216 0
img21.jpg 499.408046 139.005337 0
img21.jpg 170.628776 181.228863 0
img21.jpg 231.887269 183.495513 0
img21.jpg 289.549706 184.943596 0
img21.jpg 345.099630 186.724366 0
img21.jpg 397.291107 187.669673 0
img21.jpg 448.012251 188.937037 0
img21.jpg 495.593336 189.964319 0
img21.jpg 170.167998 238.958158 0
img21.jpg 230.558390 239.418254 0
img21.jpg 288.497209 239.644103 0
img21.jpg 342.515469 239.491195 0
img21.jpg 394.007115 239.553513 0
img21.jpg 444.372561 240.019514 0
img21.jpg 491.944262 240.141740 0
img21.jpg 169.316729 295.157425 0
img21.jpg 229.919699 293.846090 0
img21.jpg 285.963235 292.935160 0
img21.jpg 339.829832 291.170168 0
img21.jpg 391.046096 291.017874 0
img21.jpg 441.655490 290.262712 0
img21.jpg 488.484545 289.606238 0
img21.jpg 169.391253 351.146683 0
img21.jpg 227.941254 348.429636 0
img21.jpg 284.725293 345.328308 0
img21.jpg 337.367128 343.363980 0
img21.jpg 388.802075 341.231567 0
img21.jpg 438.530539 340.104779 0
img21.jpg 485.397165 338.584278 0
img21.jpg 167.777378 405.478817 0
img21.jpg 226.748250 401.012785 0
img21.jpg 282.079972 397.147880 0
img21.jpg 335.558834 393.843829 0
img21.jpg 385.729546 390.894412 0
img21.jpg 434.287148 387.675643 0
img21.jpg 480.912754 385.395124 0
img22.jpg 170.954619 66.207185 0
img22.jpg 232.925674 70.329078 0
img22.jpg 291.929905 74.309458 0
img22.jpg 348.618405 78.647759 0
img22.jpg 401.907159 82.127252 0
img22.jpg 454.073162 85.908807 0
img22.jpg 502.384304 89.612773 0
img22.jpg 170.672483 125.415122 0
img22.jpg 231.462866 128.773806 0
img22.jpg 291.032499 132.398581 0
img22.jpg 345.684337 134.915663 0
img22.jpg 399.486674 137.530287 0
img22.jpg 450.909032 139.320663 0
img22.jpg 498.894327 142.166906 0
img22.jpg 170.039566 184.623157 0
img22.jpg 231.346178 186.659774 0
img22.jpg 289.163911 188.055595 0
img22.jpg 344.132387 189.912225 0
img22.jpg 396.321555 191.204305 0
img22.jpg 447.459807 192.313964 0
img22.jpg 495.143206 193.384192 0
img22.jpg 169.696611 241.964717 0
img22.jpg 229.931254 242.875875 0
img22.jpg 288.019370 243.058687 0
img22.jpg 341.607179 243.256923 0
img22.jpg 393.716058 243.405927 0
img22.jpg 443.892988 243.204055 0
img22.jpg 491.834639 243.220219 0
img22.jpg 168.652822 299.444695 0
img22.jpg 228.891661 297.586808 0
img22.jpg 285.893939 296.780702 0
img22.jpg 340.007199 294.868026 0
img22.jpg 390.975809 294.235148 0
img22.jpg 441.273511 293.692402 0
img22.jpg 487.912647 293.046936 0
img22.jpg 168.141104 354.722532 0
img22.jpg 227.178901 352.045371 0
img22.jpg 284.087214 349.402920 0
img22.jpg 337.067904 346.926664 0
img22.jpg 388.459654 345.137176 0
img22.jpg 437.933373 343.278757 0
img22.jpg 484.875402 342.180470 0
img22.jpg 167.262818 409.231675 0
img22.jpg 225.776135 404.722118 0
img22.jpg 281.542602 401.181308 0
img22.jpg 334.813427 397.384595 0
img22.jpg 385.508089 394.555612 0
img22.jpg 434.342519 391.542815 0
img22.jpg 480.929907 388.713162 0
img25.jpg - - -
img2.jpg - - -
img16.jpg - - -
img17.jpg - - -
img5.jpg - - -
img6.jpg - - -
img9.jpg - - -
img1.jpg - - -
img20.jpg 203.417000 161.504000 0
img20.jpg 239.114000 163.886000 0
img20.jpg 273.107000 166.449000 0
img20.jpg 305.916000 168.563000 0
img20.jpg 337.115000 170.991000 0
img20.jpg 368.030000 172.639000 0
img20.jpg 397.193000 175.197000 0
img20.jpg 202.091000 195.309000 0
img20.jpg 237.449000 197.254000 0
img20.jpg 271.811000 199.126000 0
img20.jpg 303.884000 201.339000 0
img20.jpg 335.799000 202.775000 0
img20.jpg 366.476000 203.962000 0
img20.jpg 395.621000 205.477000 0
img20.jpg 200.763000 229.055000 0
img20.jpg 235.846000 230.374000 0
img20.jpg 270.150000 231.701000 0
img20.jpg 302.963000 233.051000 0
img20.jpg 334.118000 233.940000 0
img20.jpg 364.861000 235.145000 0
img20.jpg 393.631000 236.292000 0
img20.jpg 199.378000 263.126000 0
img20.jpg 234.485000 263.471000 0
img20.jpg 268.802000 263.894000 0
img20.jpg 301.142000 265.078000 0
img20.jpg 332.232000 265.327000 0
img20.jpg 363.139000 265.668000 0
img20.jpg 391.948000 266.685000 0
img20.jpg 198.029000 296.998000 0
img20.jpg 233.112000 296.261000 0
img20.jpg 266.880000 296.323000 0
img20.jpg 299.629000 296.302000 0
img20.jpg 330.424000 296.424000 0
img20.jpg 361.084000 297.002000 0
img20.jpg 389.842000 296.814000 0
img20.jpg 195.902000 329.523000 0
img20.jpg 231.127000 329.233000 0
img20.jpg 265.381000 329.073000 0
img20.jpg 297.542000 327.951000 0
img20.jpg 328.969000 326.898000 0
img20.jpg 359.115000 327.240000 0
img20.jpg 388.128000 327.274000 0
img20.jpg 194.570000 362.996000 0
img20.jpg 229.508000 361.475000 0
img20.jpg 263.279000 360.993000 0
img20.jpg 295.782000 359.146000 0
img20.jpg 326.440000 358.773000 0
img20.jpg 357.322000 357.793000 0
img20.jpg 385.821000 357.147000 0
img23.jpg 167.096579 75.563045 0
img23.jpg 229.044390 79.769831 0
img23.jpg 288.347806 83.685341 0
img23.jpg 345.133847 87.745306 0
img23.jpg 398.242697 91.371038 0
img23.jpg 450.258727 94.791385 0
img23.jpg 498.481567 98.006336 0
img23.jpg 166.457718 134.764653 0
img23.jpg 228.030388 137.434067 0
img23.jpg 287.553592 141.303498 0
img23.jpg 343.134211 143.527946 0
img23.jpg 395.604435 146.123987 0
img23.jpg 447.141897 148.657081 0
img23.jpg 495.483977 150.939440 0
img23.jpg 165.628349 193.670954 0
img23.jpg 227.436170 195.524500 0
img23.jpg 285.517103 197.434608 0
img23.jpg 341.055025 198.983142 0
img23.jpg 393.692971 199.687896 0
img23.jpg 444.170013 200.967691 0
img23.jpg 492.007105 201.736728 0
img23.jpg 165.260826 251.970544 0
img23.jpg 225.900270 251.973265 0
img23.jpg 284.098946 252.060422 0
img23.jpg 338.834414 251.788450 0
img23.jpg 390.193044 251.929631 0
img23.jpg 440.831715 251.885293 0
img23.jpg 488.336721 252.191396 0
img23.jpg 163.791281 309.386063 0
img23.jpg 224.929397 307.232453 0
img23.jpg 282.176961 305.725654 0
img23.jpg 335.984449 304.607775 0
img23.jpg 387.279070 303.196963 0
img23.jpg 437.747368 302.540835 0
img23.jpg 485.297854 301.494266 0
img23.jpg 163.968029 365.050099 0
img23.jpg 223.201096 362.148888 0
img23.jpg 280.445895 359.351900 0
img23.jpg 333.640557 356.491297 0
img23.jpg 385.611380 354.094168 0
img23.jpg 435.165143 352.729433 0
img23.jpg 482.091338 350.966958 0
img23.jpg 163.221401 419.674165 0
img23.jpg 221.857647 415.074118 0
img23.jpg 277.713041 411.798257 0
img23.jpg 331.766780 407.068828 0
img23.jpg 382.058162 404.138822 0
img23.jpg 431.852815 401.145043 0
img23.jpg 478.272757 398.091691 0
img24.jpg - - -
img3.jpg - - -
img4.jpg 325.516000 132.934000 0
img4.jpg 371.214000 134.351000 0
img4.jpg 415.623000 135.342000 0
img4.jpg 460.354000 136.823000 0
img4.jpg 504.145000 138.109000 0
img4.jpg 547.712000 139.650000 0
img4.jpg 594.000000 148.683000 0
img4.jpg 324.871000 176.873000 0
img4.jpg 369.412000 177.909000 0
img4.jpg 414.233000 179.545000 0
img4.jpg 457.929000 181.193000 0
img4.jpg 501.911000 181.665000 0
img4.jpg 545.353000 183.286000 0
img4.jpg 587.117000 184.587000 0
img4.jpg 323.335000 221.308000 0
img4.jpg 368.023000 221.689000 0
img4.jpg 412.790000 223.232000 0
img4.jpg 456.687000 223.741000 0
img4.jpg 499.676000 225.028000 0
img4.jpg 543.056000 226.144000 0
img4.jpg 584.376000 227.355000 0
img4.jpg 321.873000 264.356000 0
img4.jpg 366.604000 265.474000 0
img4.jpg 411.506000 265.928000 0
img4.jpg 454.473000 267.156000 0
img4.jpg 497.687000 267.316000 0
img4.jpg 540.800000 268.549000 0
img4.jpg 582.004000 268.906000 0
img4.jpg 321.069000 307.494000 0
img4.jpg 365.617000 308.399000 0
img4.jpg 409.188000 309.055000 0
img4.jpg 453.092000 309.161000 0
img4.jpg 495.585000 309.516000 0
img4.jpg 538.113000 310.626000 0
img4.jpg 579.114000 310.916000 0
img4.jpg 319.962000 351.063000 0
img4.jpg 363.211000 351.180000 0
img4.jpg 407.939000 351.029000 0
img4.jpg 450.832000 351.136000 0
img4.jpg 493.292000 351.660000 0
img4.jpg 535.927000 352.151000 0
img4.jpg 576.977000 352.415000 0
img4.jpg 317.523000 394.612000 0
img4.jpg 361.653000 393.122000 0
img4.jpg 405.486000 393.690000 0
img4.jpg 449.094000 393.107000 0
img4.jpg 490.867000 393.069000 0
img4.jpg 533.174000 393.251000 0
img4.jpg 573.450000 392.904000 0
img7.jpg - - -
img8.jpg - - -

View File

@@ -0,0 +1,171 @@
## generated with mrgingham --jobs 4 --gridn 7 img10.jpg img11.jpg img12.jpg img13.jpg img14.jpg img15.jpg img16.jpg img17.jpg img18.jpg img19.jpg img1.jpg img20.jpg img21.jpg img22.jpg img23.jpg img24.jpg img25.jpg img2.jpg img3.jpg img4.jpg img5.jpg img6.jpg img7.jpg img8.jpg img9.jpg
# filename x y level
img10.jpg - - -
img12.jpg - - -
img13.jpg - - -
img11.jpg - - -
img14.jpg - - -
img16.jpg 230.852688 45.677636 0
img16.jpg 290.588419 43.637631 0
img16.jpg 353.551532 40.791532 0
img16.jpg 416.562335 37.824245 0
img16.jpg 482.521266 35.024665 0
img16.jpg 550.680595 31.314791 0
img16.jpg 620.486435 27.662542 0
img16.jpg 228.101210 107.169967 0
img16.jpg 287.668764 105.636131 0
img16.jpg 350.608677 104.196417 0
img16.jpg 413.726046 102.876996 0
img16.jpg 479.587961 100.961492 0
img16.jpg 548.288420 98.337218 0
img16.jpg 617.491433 96.202379 0
img16.jpg 227.317580 170.380526 0
img16.jpg 286.573678 169.723367 0
img16.jpg 348.871750 169.077797 0
img16.jpg 412.298791 168.400691 0
img16.jpg 476.736521 167.424006 0
img16.jpg 545.646695 166.382103 0
img16.jpg 615.060574 164.978558 0
img16.jpg 224.988291 232.652880 0
img16.jpg 284.444268 233.052725 0
img16.jpg 347.369088 233.269971 0
img16.jpg 410.603748 233.382944 0
img16.jpg 474.877702 233.449657 0
img16.jpg 542.719183 233.403859 0
img16.jpg 612.559443 233.450298 0
img16.jpg 224.176494 293.917407 0
img16.jpg 283.560852 295.883657 0
img16.jpg 344.909937 296.351158 0
img16.jpg 407.748291 297.773360 0
img16.jpg 473.420095 299.484169 0
img16.jpg 540.155696 299.594645 0
img16.jpg 610.001831 302.013660 0
img16.jpg 222.765937 356.286965 0
img16.jpg 281.624273 358.045270 0
img16.jpg 344.401747 359.708573 0
img16.jpg 407.178146 362.510875 0
img16.jpg 470.802112 364.398990 0
img16.jpg 538.976097 366.428889 0
img16.jpg 608.179152 368.768331 0
img16.jpg 221.298532 417.483438 0
img16.jpg 280.640228 420.040118 0
img16.jpg 341.602139 422.765487 0
img16.jpg 404.672431 425.770751 0
img16.jpg 470.190240 428.866733 0
img16.jpg 536.654210 431.757569 0
img16.jpg 605.530880 434.718486 0
img17.jpg - - -
img15.jpg - - -
img18.jpg - - -
img1.jpg - - -
img20.jpg - - -
img19.jpg - - -
img21.jpg - - -
img23.jpg - - -
img22.jpg 94.983538 191.554631 0
img22.jpg 153.614577 190.963557 0
img22.jpg 214.504831 189.801771 0
img22.jpg 275.163494 188.729227 0
img22.jpg 335.692361 188.261850 0
img22.jpg 398.653076 187.468366 0
img22.jpg 461.538759 185.425839 0
img22.jpg 90.898174 249.378532 0
img22.jpg 149.890629 248.474019 0
img22.jpg 212.238288 248.464675 0
img22.jpg 272.692903 248.378072 0
img22.jpg 335.315339 248.340460 0
img22.jpg 397.425010 247.241587 0
img22.jpg 460.113872 246.273172 0
img22.jpg 88.271128 308.883542 0
img22.jpg 148.111111 308.903030 0
img22.jpg 209.528269 308.368223 0
img22.jpg 272.122948 308.566463 0
img22.jpg 333.255342 308.516094 0
img22.jpg 396.217537 308.400373 0
img22.jpg 458.998879 308.298711 0
img22.jpg 85.549180 369.109836 0
img22.jpg 145.604505 368.636110 0
img22.jpg 207.843879 368.708637 0
img22.jpg 269.426938 368.718688 0
img22.jpg 332.511083 368.696887 0
img22.jpg 395.694279 368.662189 0
img22.jpg 458.724238 368.692204 0
img22.jpg 83.324617 430.237009 0
img22.jpg 143.464362 430.395213 0
img22.jpg 205.934830 430.915077 0
img22.jpg 268.120193 431.258451 0
img22.jpg 331.145142 431.202329 0
img22.jpg 395.505179 431.334136 0
img22.jpg 458.594962 431.563843 0
img22.jpg 80.427088 491.525255 0
img22.jpg 140.407319 491.866495 0
img22.jpg 203.493586 492.298204 0
img22.jpg 266.502687 493.153395 0
img22.jpg 329.540655 493.438226 0
img22.jpg 394.060910 493.977069 0
img22.jpg 458.576923 494.657254 0
img22.jpg 78.230002 554.336896 0
img22.jpg 138.799033 555.027797 0
img22.jpg 201.690901 555.788772 0
img22.jpg 265.045455 556.634769 0
img22.jpg 329.438927 557.467018 0
img22.jpg 392.902428 557.683931 0
img22.jpg 458.234614 557.851508 0
img24.jpg - - -
img3.jpg 375.342835 136.998277 0
img3.jpg 434.057516 131.808345 0
img3.jpg 495.305888 126.738275 0
img3.jpg 560.364821 120.983028 0
img3.jpg 626.983524 114.899674 0
img3.jpg 698.646341 108.293485 0
img3.jpg 773.638004 101.614613 0
img3.jpg 373.353018 197.423392 0
img3.jpg 431.352319 193.725590 0
img3.jpg 493.723975 189.762005 0
img3.jpg 557.749077 185.410162 0
img3.jpg 626.204480 181.344086 0
img3.jpg 698.555333 176.339163 0
img3.jpg 773.225717 171.736577 0
img3.jpg 371.364100 257.700753 0
img3.jpg 428.934194 255.808344 0
img3.jpg 491.401929 254.033762 0
img3.jpg 556.084802 251.522082 0
img3.jpg 623.939787 248.292497 0
img3.jpg 697.030951 245.615081 0
img3.jpg 773.206566 242.407319 0
img3.jpg 369.222475 319.052254 0
img3.jpg 428.045774 317.745414 0
img3.jpg 489.579236 317.512243 0
img3.jpg 554.593832 317.152788 0
img3.jpg 623.120840 315.677892 0
img3.jpg 695.965810 314.573428 0
img3.jpg 772.324664 314.552466 0
img3.jpg 368.363534 380.504822 0
img3.jpg 425.651057 380.679758 0
img3.jpg 488.723907 381.062698 0
img3.jpg 552.911222 382.782819 0
img3.jpg 621.428571 383.677943 0
img3.jpg 695.473001 383.910764 0
img3.jpg 771.833725 385.933615 0
img3.jpg 365.431290 441.297896 0
img3.jpg 424.516816 443.794381 0
img3.jpg 486.589463 446.591610 0
img3.jpg 551.710297 449.411885 0
img3.jpg 620.795019 452.041878 0
img3.jpg 695.052279 455.202786 0
img3.jpg 770.902807 457.918805 0
img3.jpg 364.619850 504.218022 0
img3.jpg 422.691676 508.208675 0
img3.jpg 485.547412 512.388199 0
img3.jpg 551.785073 516.002626 0
img3.jpg 620.657118 521.103367 0
img3.jpg 694.273407 525.069776 0
img3.jpg 770.430863 529.703696 0
img25.jpg - - -
img2.jpg - - -
img4.jpg - - -
img7.jpg - - -
img5.jpg - - -
img6.jpg - - -
img8.jpg - - -
img9.jpg - - -