-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Samples: Add sample to run infield repeatedly
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
Showing
3 changed files
with
279 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
85
source/camera/maintenance/analyze_camera_characterisitics.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
189
source/camera/maintenance/characterize_camera_in_field.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |