Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
* Ask questions about the prompt to clarify any uncertainties.
* Ensure that new code compiles without warnings or errors.
* Prioritize making new code efficient and compact
* Prefer file scoped namespaces
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ PhotoLocatorSetup/*.pfx
PhotoLocatorSetup/AppPackages/
PhotoLocatorSetup/BundleArtifacts/
TestResults/
__pycache__/
publish.bat
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"name": "C#: PhotoLocator Debug",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/PhotoLocator/bin/Debug/net8.0-windows/PhotoLocator.exe",
"program": "${workspaceFolder}/PhotoLocator/bin/Debug/net10.0-windows/PhotoLocator.exe",
"args": [],
"cwd": "${workspaceFolder}/PhotoLocator/bin/Debug/net8.0-windows/",
"cwd": "${workspaceFolder}/PhotoLocator/bin/Debug/net10.0-windows/",
"stopAtEntry": false,
"preLaunchTask": "build"
}
Expand Down
41 changes: 41 additions & 0 deletions PhotoLocator/AI/create_minimal_denoise_test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from pathlib import Path

import onnx
from onnx import TensorProto, helper, numpy_helper


def create_identity_model(output_path: Path):
input_tensor = helper.make_tensor_value_info("input", TensorProto.FLOAT, ["batch", "channels", "height", "width"])
output_tensor = helper.make_tensor_value_info("output", TensorProto.FLOAT, ["batch", "channels", "height", "width"])

identity_node = helper.make_node(
"Identity",
inputs=["input"],
outputs=["output"],
name="identity_test",
)

graph = helper.make_graph(
[identity_node],
"minimal_denoise_test_graph",
[input_tensor],
[output_tensor],
)

model = helper.make_model(graph, producer_name="PhotoLocatorTest")
model.opset_import[0].version = 17
model.ir_version = 13

output_path.parent.mkdir(parents=True, exist_ok=True)
onnx.save(model, str(output_path))
print(f"Saved minimal ONNX smoke-test model to {output_path}")


def main():
repo_root = Path(__file__).resolve().parents[2]
output_path = repo_root / "PhotoLocatorTest" / "TestData" / "dncnn_test.onnx"
create_identity_model(output_path)


if __name__ == "__main__":
main()
41 changes: 41 additions & 0 deletions PhotoLocator/AI/denoise_dncnn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import argparse
from pathlib import Path

import cv2
import torch

from train_dncnn import DnCNN, checkpoint_model_state, denoise_image


def main():
parser = argparse.ArgumentParser(description="Denoise an image using a trained PyTorch DnCNN checkpoint.")
parser.add_argument("--model", type=str, required=True, help="Path to the trained PyTorch checkpoint (.pt)")
parser.add_argument("--input", type=str, required=True, help="Path to the image to denoise")
parser.add_argument("--output", type=str, required=True, help="Path for the denoised image")
parser.add_argument("--channels", type=int, default=3, choices=[1, 3], help="Model input channels")
parser.add_argument("--tile-size", type=int, default=512, help="Maximum tile width and height")
parser.add_argument("--overlap", type=int, default=64, help="Overlap between adjacent inference tiles")
parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu", help="Inference device, e.g. cpu or cuda")
args = parser.parse_args()

image = cv2.imread(args.input, cv2.IMREAD_UNCHANGED)
if image is None:
raise FileNotFoundError(f"Unable to read input image: {args.input}")

model = DnCNN(channels=args.channels).to(args.device)
checkpoint = torch.load(args.model, map_location=args.device)
model.load_state_dict(checkpoint_model_state(checkpoint))
print(f"Inference device: {args.device}")
result = denoise_image(model, image, args.channels, args.device, args.tile_size, args.overlap)
if result.shape[2] == 1:
result = result[:, :, 0]

output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
if not cv2.imwrite(str(output_path), result):
raise OSError(f"Unable to save denoised image: {output_path}")
print(f"Saved denoised image to {output_path}")


if __name__ == "__main__":
main()
57 changes: 57 additions & 0 deletions PhotoLocator/AI/export_dncnn_onnx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import argparse
import os

import onnx
import torch

from train_dncnn import DnCNN, checkpoint_model_state


def main():
parser = argparse.ArgumentParser(description="Export a trained DnCNN denoiser to a single-file embedded ONNX model.")
parser.add_argument("--model", type=str, default="dncnn_model.pt", help="Path to the saved PyTorch weights (.pt)")
parser.add_argument("--output", type=str, default="dncnn_model.onnx", help="Path for the ONNX model")
parser.add_argument("--channels", type=int, default=3, choices=[1, 3], help="Input image channels")
parser.add_argument("--height", type=int, default=256, help="Example input height")
parser.add_argument("--width", type=int, default=256, help="Example input width")
parser.add_argument("--opset", type=int, default=17, help="ONNX opset version")
args = parser.parse_args()

model = DnCNN(channels=args.channels)
checkpoint = torch.load(args.model, map_location="cpu")
model.load_state_dict(checkpoint_model_state(checkpoint))
model.eval()

dummy_input = torch.randn(1, args.channels, args.height, args.width)
temp_path = args.output + ".tmp"
torch.onnx.export(
model,
(dummy_input,),
temp_path,
export_params=True,
opset_version=args.opset,
do_constant_folding=True,
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size", 2: "height", 3: "width"},
"output": {0: "batch_size", 2: "height", 3: "width"},
},
)

model_onnx = onnx.load(temp_path)
onnx.save_model(
model_onnx,
args.output,
save_as_external_data=False,
all_tensors_to_one_file=True,
)

os.remove(temp_path)
os.remove(temp_path + ".data")

print(f"Exported ONNX model to {args.output}")


if __name__ == "__main__":
main()
Loading
Loading