Skip to content

Commit

Permalink
Samples: Add sample to run infield repeatedly
Browse files Browse the repository at this point in the history
These samples allow a customer to analyze how their camera behaves
under their specific conditions. These conditions include;
- capture interval
- non-operating periods
- ambient temperature
- mounting considerations

The first sample collects data in a CSV file. The second sample
uses Pandas and Matplotlib to plot the results.

MISC
  • Loading branch information
torbsorb committed Jun 12, 2023
1 parent 48c5d3a commit 89df4c9
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 4 deletions.
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
numpy
matplotlib
opencv-python
numpy
open3d
opencv-python
pandas
pyyaml
scipy
robodk
zivid
scipy
zivid
85 changes: 85 additions & 0 deletions source/camera/maintenance/analyze_camera_characterisitics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import argparse
from pathlib import Path

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import pandas as pd


def _options() -> argparse.Namespace:
"""Function to read user arguments
Returns:
Argument from user
"""
parser = argparse.ArgumentParser(description=__doc__)

parser.add_argument(
"--csv-path",
required=False,
type=Path,
default=Path(__file__).parent / "verification_data.csv",
help="Path to the verification data",
)

return parser.parse_args()


def _main():
user_input = _options()

df = pd.read_csv(user_input.csv_path, parse_dates=["time"], comment="#")
temperature_labels = [label for label in df.columns if label.startswith("temperature.")]

fig, axes = plt.subplots(2, 2, figsize=(16, 8), sharex=True)
temperature_axes = [axes[0,0], axes[0,1], axes[1,1]]
axes[1,0].axis("off")
ax_dim_trueness = axes[0,0].twinx()
ax_position_drift = axes[0,1].twinx()

# Plot temperature variables
for column in temperature_labels:
label = label=column.replace("temperature.","")
axes[1,1].plot(df["time"], df[column], label=label, linestyle='dashed')
if "DMD" in label:
axes[0,0].plot(df["time"], df[column], label=label, linestyle='dashed')
axes[0,1].plot(df["time"], df[column], label=label, linestyle='dashed')

# Plot dimension_trueness
ax_dim_trueness.plot(df["time"], df["dimension_trueness"] * 100, "+-r", label="dimension_trueness")
ax_dim_trueness.set_ylim([0, max(df["dimension_trueness"].max() * 100, 0.1)])

# Plot position drift
for label in ['x', 'y', 'z']:
ax_position_drift.plot(df["time"], df[f"position.{label}"] - df[f"position.{label}"][0], label=label)

# Set labels and title
for ax, title in zip(temperature_axes, ["Dimension Trueness", "Positional Drift", "All Temperatures"]):
ax.set_xlabel("Time")
ax.set_ylabel("\u00b0C", rotation=0)
ax.set_title(title)
formatter = ticker.FuncFormatter(lambda x, _: f"{x:.2f}%")
ax_dim_trueness.yaxis.set_major_formatter(formatter)
ax_dim_trueness.set_ylabel("Dimension Trueness")
formatter = ticker.FuncFormatter(lambda x, _: f"{x:.2f}")
ax_position_drift.yaxis.set_major_formatter(formatter)
ax_position_drift.set_ylabel("mm", rotation=0)

# Display legend
for ax in temperature_axes:
ax.legend(loc="center left")
ax_position_drift.legend(loc="center right")

# Display meta data
meta_data = "\n".join([line for line in Path(user_input.csv_path).read_text(encoding="utf-8").splitlines() if line.startswith("#")])
fig.text(0.05, 0.25, meta_data, ha='left', va='center', fontsize=12, transform=fig.transFigure)

# Show the plot
fig.tight_layout()
plt.show()


if __name__ == "__main__":
_main()
189 changes: 189 additions & 0 deletions source/camera/maintenance/characterize_camera_in_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"""
Check the dimension trueness of a Zivid camera.
This example shows how to verify the local dimension trueness of a camera.
If the trueness is much worse than expected, the camera may have been damaged by
shock in shipping in handling. If so, look at the correct_camera_in_field sample sample.
Note: This example uses experimental SDK features, which may be modified, moved, or deleted in the future without notice.
"""

import argparse
import csv
import random
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Union

import zivid
from zivid.experimental import calibration


def _options() -> argparse.Namespace:
"""Function to read user arguments
Returns:
Argument from user
"""
parser = argparse.ArgumentParser(description=__doc__)

parser.add_argument(
"--csv-path",
required=False,
type=Path,
default=Path(__file__).parent / "verification_data.csv",
help="Path to the verification data",
)

return parser.parse_args()


@dataclass
class RandomCaptureCycle:
min_captures: int = 3
max_captures: int = 15
min_capture_interval: timedelta = timedelta(seconds=5)
max_capture_interval: timedelta = timedelta(seconds=15)

def __post_init__(self) -> None:
random.seed(datetime.now().microsecond)

def capture_interval(self) -> timedelta:
return timedelta(
seconds=random.uniform(self.min_capture_interval.total_seconds(), self.max_capture_interval.total_seconds())
)

def number_of_captures(self) -> int:
return random.randint(self.min_captures, self.max_captures)


@dataclass
class StandbyCycle:
total_duration: timedelta = timedelta(minutes=30)
sample_interval: timedelta = timedelta(seconds=30)


@dataclass
class VerificationAndState:
verification: calibration.CameraVerification
info: zivid.CameraInfo
state: zivid.CameraState
time: str

def __str__(self) -> str:
return f"""\
Time: {self.time}
Pose: {'x':>4}:{self.verification.position()[0]:>8.3f} mm{'y':>4}:{self.verification.position()[1]:>8.3f} mm{'z':>4}:{self.verification.position()[2]:>8.3f} mm
Estimated dimension trueness: {self.verification.local_dimension_trueness()*100:.3f}%
Temperatures:
{'DMD:':<9}{self.state.temperature.dmd:>6.1f}\u00b0C
{'Lens:':<9}{self.state.temperature.lens:>6.1f}\u00b0C
{'LED:':<9}{self.state.temperature.led:>6.1f}\u00b0C
{'PCB:':<9}{self.state.temperature.pcb:>6.1f}\u00b0C
{'General:':<9}{self.state.temperature.general:>6.1f}\u00b0C
"""

def as_dict(self) -> dict:
return {
"time": self.time,
"position.x": self.verification.position()[0],
"position.y": self.verification.position()[1],
"position.z": self.verification.position()[2],
"dimension_trueness": self.verification.local_dimension_trueness(),
"temperature.DMD": self.state.temperature.dmd,
"temperature.Lens": self.state.temperature.lens,
"temperature.LED": self.state.temperature.led,
"temperature.PCB": self.state.temperature.pcb,
"temperature.General": self.state.temperature.general,
}


def _measure(camera: zivid.Camera) -> VerificationAndState:
time_string = time.strftime("%Y-%m-%d %H:%M:%S")
detection_result = calibration.detect_feature_points(camera)
camera_state = camera.state
infield_input = calibration.InfieldCorrectionInput(detection_result)
if not infield_input.valid():
raise RuntimeError(
f"Capture not valid for infield verification! Feedback: {infield_input.status_description()}"
)
return VerificationAndState(
verification=calibration.verify_camera(infield_input),
info=camera.info,
state=camera_state,
time=time_string,
)


def append_to_csv_file(csv_path: Path, verification_data: VerificationAndState) -> None:
with open(csv_path, "a", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=verification_data.as_dict().keys())
writer.writerow(verification_data.as_dict())


def start_csv_file(
csv_path: Path, verification_data: VerificationAndState, camera_correction_timestamp: Union[datetime, None]
) -> None:
with open(csv_path, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=verification_data.as_dict().keys())
csvfile.write("\n".join([f"# {line}" for line in str(verification_data.info).splitlines()]))
csvfile.write("\n")
if camera_correction_timestamp:
csvfile.write(
f"# Timestamp of current camera correction: {camera_correction_timestamp.strftime(r'%Y-%m-%d %H:%M:%S')}"
)
csvfile.write("\n")
else:
csvfile.write("# This camera has no infield correction written to it.")
csvfile.write("\n")
writer.writeheader()


def standby_loop(camera: zivid.Camera, csv_path: Path, standby_cycle: StandbyCycle) -> None:
end_time = datetime.now() + standby_cycle.total_duration
start_of_loop = datetime.now()
while start_of_loop < end_time:
verification_data = _measure(camera)
print(verification_data)
append_to_csv_file(csv_path, verification_data)
time.sleep((standby_cycle.sample_interval - (datetime.now() - start_of_loop)).total_seconds())
start_of_loop = datetime.now()


def random_capture_loop(camera: zivid.Camera, csv_path: Path, timing: RandomCaptureCycle) -> None:
for _ in range(timing.number_of_captures()):
verification_data = _measure(camera)
print(verification_data)
append_to_csv_file(csv_path, verification_data)
time.sleep(timing.capture_interval().total_seconds())


def _main() -> None:
user_input = _options()

app = zivid.Application()

print("Connecting to camera")
camera = app.connect_camera()

camera_correction_timestamp = (
calibration.camera_correction_timestamp(camera) if calibration.has_camera_correction(camera) else None
)
initial_verification = _measure(camera)
print(initial_verification)
start_csv_file(user_input.csv_path, initial_verification, camera_correction_timestamp)

standby_cycle = StandbyCycle()
random_capture_cycle = RandomCaptureCycle()
while True:
standby_loop(camera, user_input.csv_path, standby_cycle)
random_capture_loop(camera, user_input.csv_path, random_capture_cycle)


if __name__ == "__main__":
_main()

0 comments on commit 89df4c9

Please sign in to comment.