def encrypt_png(self, in_path: PathType, out_path: PathType) -> None:
        image = PngImageFile(in_path)

        # convert pixels to bytes in order to encrypt them
        im_bytes = bytearray(self.__get_pixels(image))

        # get random IV and calculate MAC
        iv = get_random_bytes(CryptoAES.block_size)
        h = HMAC.new(self.__key, digestmod=SHA256)
        h.update(im_bytes)

        # create metadata object in order to save IV and MAC to image
        metadata = PngInfo()
        metadata.add_text('iv', iv.hex())
        metadata.add_text('mac', h.hexdigest())

        print(f'writing IV = {iv.hex()} and MAC = {h.hexdigest()} to image metadata')

        # encrypt image
        cipher = CryptoAES.new(self.__key, CryptoAES.MODE_ECB)
        enc_data = cipher.encrypt(im_bytes)

        # write image to file with metadata
        image.frombytes(enc_data)
        image.save(out_path, pnginfo=metadata)
Esempio n. 2
0
def add_tags_to_png_file(fpath):
    try:
        info = create_file_info(fpath)
        png_image = PngImageFile(open(fpath, 'rb'))
        png_info = PngInfo()
        for k, v in info.items():
            png_info.add_text(k, v)
        png_image.save(fpath, pnginfo=png_info)
    except (Exception, OSError):
        print("WARNING: Could not add debug info to file '{}'.".format(fpath))
        traceback.print_exc()
Esempio n. 3
0
 def savemeta(*args, **kwargs):
     path = args[1]
     if path.endswith(".fig"):
         import pickle as pkl
         pkl.dump(args[0], open(path, 'wb'))
     else:
         mpl_savefig(*args, **kwargs)
         #fig = args[0]
         if path.endswith(".png"):
             targetImage = PngImageFile(path)
             metadata = PngInfo()
             metadata.add_text("Description", str(meta))
             targetImage.save(path, pnginfo=metadata)
Esempio n. 4
0
def write_png_metadata(filename, settings):
    targetImage = PngImageFile(filename)
    metadata = PngInfo()
    for (k, v) in settings.items():
        if type(v) == list:
            value = ""
            for item in v:
                value += str(item) + " "
            v = value
        if type(v) == bool:
            v = str(v)
        if v is None:
            continue
        else:
            metadata.add_text(k, str(v))
    targetImage.save(filename, pnginfo=metadata)
Esempio n. 5
0
    def sample_image_path(self):
        """Copy the sample image to to a unique file name and return the path.

		Returns:
			tuple of (the filename, the new sample image path)
		"""
        filename = self.generate_alphanumeric() + ".png"
        new_file = self.tmp_file_path(filename)
        shutil.copy(self.original_sample_image_path, new_file)

        # make the image content unique
        image_file = PngImageFile(open(new_file, "r"))
        info = PngInfo()
        info.add_text('Comment', self.generate_alphanumeric(length=30))
        image_file.save(new_file, pnginfo=info)
        self.temp_files.append(new_file)
        return (filename, new_file)
    def decrypt_png(self, in_path: PathType, out_path: PathType) -> None:
        image = PngImageFile(in_path)
        iv: Optional[str] = None
        mac: Optional[str] = None

        # try to get IV from metadata
        try:
            iv = image.text['iv']

            print(f'found IV = {iv}')
        except KeyError:
            print('IV was not found in file')

        # try to get MAC from metadata
        try:
            mac = image.text['mac']

            print(f'found MAC = {mac}')
        except KeyError:
            print('MAC was not found in file')

        # convert pixels to bytes in order to decrypt them
        im_bytes = bytearray(self.__get_pixels(image))

        # decrypt image
        cipher = CryptoAES.new(self.__key, CryptoAES.MODE_ECB)
        dec: bytes = cipher.decrypt(im_bytes)

        # try to verify MAC
        try:
            self.__hmac.update(dec)
            self.__hmac.verify(bytes.fromhex(mac))

            print('MAC is valid')
        except ValueError:
            print('MAC is invalid')

        # don't forget about metadata
        metadata = PngInfo()
        metadata.add_text('iv', iv)
        metadata.add_text('mac', mac)

        # save decrypted image to file
        image.frombytes(dec)
        image.save(out_path, pnginfo=metadata)
Esempio n. 7
0
def replace_meta(filename, fields):
    metaname = get_dumpfile(filename)
    with open(metaname) as json_file:
        meta = json.load(json_file)
    if fields and fields[0] != '*':
        print(f"overwriting metadata[{fields}] in {filename} from {metaname}")
        newmeta = {}
        for f in fields:
            newmeta[f] = meta[f]
    else:
        print(f"overwriting metadata in {filename} from {metaname}")
        newmeta = meta

    newmeta['Metadata Modification Time'] = f"{datetime.now()}"
    img = PngImageFile(filename)
    metadata = PngInfo()
    for f in newmeta:
        metadata.add_text(f, newmeta[f])
    img.save(filename, pnginfo=metadata)
Esempio n. 8
0
    def on_save_clicked(self, button):
        current_path = self.entry.get_text()

        # write metadata
        metadata = PngInfo()
        metadata.add_text("screenshat", self.entryy.get_text())

        img = PngImageFile(img_path)
        img.save(img_path, pnginfo=metadata)
        

        # Update config file
        pathlib.Path(config_path).write_text(current_path)
        
        # in doubt do mkdir -p new directory
        pathlib.Path(current_path).mkdir(parents=True, exist_ok=True)

        # move file 
        shutil.move(img_path, current_path)

        self.destroy()
Esempio n. 9
0
def optimize_png(img: PngImageFile,
                 tmp_file: Any,
                 quality: int = 95) -> PngImageFile:
    output_path = "/tmp/" + str(uuid.uuid4())

    if quality >= 100:
        return img

    if quality % 10 == 0:
        speed = int(quality /
                    10)  # quality is inversely related to pngquant speed
    else:
        # each interval of 10 is related to one speed:
        # [90 - 99] --> speed 9
        speed = int(floor((quality - (quality % 10)) / 10))

    command = [
        PNGQUANT_PATH,
        "--strip",
        "--force",
        "--output",
        output_path,
        "-s" + str(speed),
        tmp_file.name,
    ]

    try:
        img.save(tmp_file.name, format=img.format)
        subprocess.check_output(command, stderr=subprocess.STDOUT)
        img = Image.open(output_path)
        if Path(output_path).exists():
            Path(output_path).unlink()
    except (OSError, subprocess.CalledProcessError) as error:
        raise ITSTransformError("ITSTransform Error: " + str(error))

    return img
Esempio n. 10
0
def rgbChannels(inFile: PIL.PngImagePlugin.PngImageFile, message: str="", quantizationWidths: list=[],
                    traversalOrder: list=[], outFile="./IO/outColor.png", verify: bool=False, verbose: bool=False):
    """
    This function takes takes an image of the form
    [
        [<RGB 1>, <RGB 2>, <RGB 3>, ... ],
        [<RGB a>, <RGB a+1>, <RGB a+2>, ... ],
        [<RGB b>, <RGB b+1>, <RGB b+2>, ... ],
        ...
    ]
    where RGB <index> is of the form [R, G, B] (if there is an Alpha channel present, it is ignored)

    And utilizes a modified version Wu and Tsai's algorithm to encode a message into this nested array structure.

    Because this image is RGB, an order of traversal is needed to ensure the correct encoding/retrieval order 
    while traversing the structure.
    
    Define a general pair of RGB pixels as [[R1, G1, B1], [R2, G2, B2]] and flatten it into [R1, G1, B1, R2, G2, B2]
    The traversal order is an array of that maps the corresponding value to a location it should be sorted to. 
    After mapping and sorting the pixel values, pair adjacent pixels 

    For example, a possible traversal order is the standard [1, 3, 5, 2, 4, 6]
    Applying this traversal order concept to the RGB pixel pair 
    [[185, 75, 250], [255, 80, 200]] 
    results in these encodable groups of values:
    [[185, 255], [75, 80], [250, 200]]
    """

    # Verify image data
    if verify:
        print("Beginning verification...")
        
        if message == "":
            try:
                verificationData = inFile.text["png:fingerprint"].split(":")
            except:
                raise Exception(f"No verification data found.")
            
            # Retrieve verifiable data from image properties
            imageWidth, imageHeight = rosenburgStrongPairing(int(verificationData[0]), reverse=True)
            bitLength, messageHash = retrieveLength(verificationData[1])

            # Image dimensions are incorrect
            if inFile.size[0] != imageWidth or inFile.size[1] != imageHeight:
                raise Exception(f"Image verification failed. Image dimensions don't match encoded verification data.")

            # Execute function without verifying data option
            retrievedBinary = rgbChannels(inFile, message, quantizationWidths, traversalOrder, outFile, verbose=verbose)
            
            # Ensure entire message was encoded
            if len(retrievedBinary) >= bitLength:
                retrievedBinary = retrievedBinary[:bitLength]

                # Ensure hashes match
                if hashlib.sha256(retrievedBinary.encode()).hexdigest() == messageHash:
                    print("\nVerified.")
                    return retrievedBinary
                else:
                    raise Exception(f"Message verification failed. Hash of retrieved binary doesn't match encoded verification data.")
            raise Exception("Message verification failed. Length of retrieved message binary doesn't match encoded verification data.")
        else:
            # Get binary of message
            if sorted(set(message)) == ["0", "1"]:
                messageBinary = message
            else:
                messageBinary = "0" + str(bin(int.from_bytes(message.encode(), "big")))[2:]
                
            returnValue = rgbChannels(inFile, messageBinary, quantizationWidths, traversalOrder, outFile, verbose=verbose)

            # Build verification data to place in loaded image properties
            verificationBuilder = ""
            verificationBuilder += f"{str(rosenburgStrongPairing([inFile.size[0], inFile.size[1]]))}:"
            verificationBuilder += f"{embedLength(str(len(messageBinary)), messageBinary)}"

            # Edit PNG metadata to include fingerprint of this PVD algorithm
            modifyMetadata = PngImageFile(outFile)
            metadata = PngInfo()
            metadata.add_text("png:fingerprint", f"{verificationBuilder}")
            modifyMetadata.save(outFile, pnginfo=metadata)

            print("\nVerified.")
            return returnValue

    print()

    if message == "":
        if verbose:
            print("Verbose message: no message given, assuming retrieval of message")
    else:
        # Get binary of message
        if sorted(set(message)) == ["0", "1"]:
            messageBinary = message
            if verbose:
                print("Verbose message: message contains only binary values, assuming binary message")
        else:
            messageBinary = "0" + str(bin(int.from_bytes(message.encode(), "big")))[2:]
            if verbose:
                print("Verbose message: message contains non-binary values, assuming ascii message")

    quantizationWidths = validateQuantization(quantizationWidths, verbose)
    traversalOrder = validateTraversal(traversalOrder, verbose)

    print()

    # If there is an Alpha channel present in the image, it is ignored
    pixelPairs = pixelArrayToZigZag(inFile, 3, 2)

    # If function is run without message, assume retrieval of message
    if message == "":
        print(f"Retrieving binary from file \"{inFile.filename}\"")
        print()

        # Retrieval function
        messageBinary = ""

        currentPairCounter = 0
        for pixelPair in pixelPairs:
            currentPairCounter += 1
            if len(pixelPair) == 2:
                # Flatten pixel pair array into un-nested list
                pixelArray = [pixel for pair in pixelPair for pixel in pair]

                # Sort pixel array given traversal order and group into calculation ready pairs
                pixelIndicesDict = dict(sorted(dict(zip(traversalOrder, pixelArray)).items()))
                traversedPixelPairs = list(groupImagePixels(list(pixelIndicesDict.values()), 2))

                currentTraversedCounter = 0
                for traversedPixelPair in traversedPixelPairs:
                    currentTraversedCounter += 1
                    # d value
                    difference = traversedPixelPair[1] - traversedPixelPair[0]

                    # Determine number of bits storable between pixels
                    for width in quantizationWidths:
                        if width[0] <= abs(difference) <= width[1]:
                            lowerBound = width[0]
                            upperBound = width[1]
                            break
                    
                    # Falling-off-boundary check; ensure 0 < calculated pixel value < 255
                    testingPair = pixelPairEncode(traversedPixelPair, upperBound, difference)
                    if testingPair[0] < 0 or testingPair[1] < 0 or testingPair[0] > 255 or testingPair[1] > 255:
                        # One of the values "falls-off" the range from 0 to 255 and hence is invalid
                        if verbose == True:
                            print(f"Verbose message: channel pair number {currentTraversedCounter} in pixel pair number {currentPairCounter} has the possibility of falling off, skipping")
                    else:
                        # Passes the check, continue with decoding
                        # Number of storable bits between two pixels
                        storableCount = int(math.log(upperBound - lowerBound + 1, 2))
                        
                        # Extract encoded decimal
                        retrievedDecimal = difference - lowerBound if difference >= 0 else - difference - lowerBound
                        retrievedBinary = bin(retrievedDecimal).replace("0b", "")

                        # Edge case in which embedded data began with 0's
                        if storableCount > len(retrievedBinary):
                            retrievedBinary = "0" * (storableCount-len(retrievedBinary)) + retrievedBinary

                        messageBinary += retrievedBinary
        
        return messageBinary
    else:
        print(f"Encoding binary \"{messageBinary}\" into file \"{inFile.filename}\"")
        print()

        # Encoding function
        newPixels = []

        currentMessageIndex = 0

        currentPairCounter = 0
        for pixelPair in pixelPairs:
            currentPairCounter += 1
            if len(pixelPair) == 2 and currentMessageIndex < len(messageBinary) - 1:

                # Flatten pixel pair array into un-nested list
                pixelArray = [pixel for pair in pixelPair for pixel in pair]

                # Sort pixel array given traversal order and group into calculation ready pairs
                traversalIndiceDict = list(zip(traversalOrder, [0,1,2,3,4,5]))
                pixelIndicesDict = dict(sorted(dict(zip(traversalIndiceDict, pixelArray)).items()))
                traversedPixelPairs = list(groupImagePixels(list(pixelIndicesDict.values()), 2))

                postEncodingValues = []

                currentTraversedCounter = 0
                for traversedPixelPair in traversedPixelPairs:
                    currentTraversedCounter += 1
                    # d value
                    difference = traversedPixelPair[1] - traversedPixelPair[0]

                    # Determine number of bits storable between pixels
                    for width in quantizationWidths:
                        # Only need to check upper bound because widths are sorted
                        if abs(difference) <= width[1]:
                            lowerBound = width[0]
                            upperBound = width[1]
                            break

                    # Falling-off-boundary check; ensure 0 < calculated pixel value < 255
                    testingPair = pixelPairEncode(traversedPixelPair, upperBound, difference)
                    if testingPair[0] < 0 or testingPair[1] < 0 or testingPair[0] > 255 or testingPair[1] > 255:
                        # One of the values "falls-off" the range from 0 to 255 and hence is invalid
                        # Append original pixel pair and skip encoding
                        postEncodingValues += traversedPixelPair
                        if verbose:
                            print(f"Verbose message: channel pair number {currentTraversedCounter} in pixel pair number {currentPairCounter} has the possibility of falling off, skipping")
                    else:
                        # Passes the check, continue with encoding
                        # Number of storable bits between two pixels
                        storableCount = int(math.log(upperBound - lowerBound + 1, 2))

                        # Ensure haven't already finished encoding entire message
                        if currentMessageIndex + storableCount <= len(messageBinary):
                            # Encode as normal
                            storableBits = messageBinary[currentMessageIndex:currentMessageIndex+storableCount]
                            currentMessageIndex += storableCount
                        else:
                            if currentMessageIndex == len(messageBinary):
                                # Finished encoding entire message
                                postEncodingValues += traversedPixelPair
                                continue
                            else:
                                # Can encode more bits than available, encode what's left
                                storableBits = messageBinary[currentMessageIndex:]

                                # Ensure last bit doesn't get corrupted, fill empty space with 0's
                                storableBits += "0" * (storableCount - len(messageBinary[currentMessageIndex:]))
                                currentMessageIndex = len(messageBinary)

                        # Get value of the chunk of message binary
                        storableBitsValue = int(storableBits, 2)

                        # d' value
                        differencePrime = lowerBound + storableBitsValue if difference >= 0 else -(lowerBound + storableBitsValue)

                        # Calculate new pixel pair
                        newPixelPair = pixelPairEncode(traversedPixelPair, differencePrime, difference)                    
                        postEncodingValues += newPixelPair

                # Un-sort pixel array given traversal order and group into calculation original RGB channels
                pixelIndicesDict = dict(sorted(dict(zip([ key[1] for key in pixelIndicesDict.keys() ], postEncodingValues)).items()))
                reversedPaired = list(groupImagePixels([pixel for pixel in pixelIndicesDict.values()], 3))

                newPixels += reversedPaired
            else:
                # For case in which there's an odd number of pixels; append lone pixel value
                newPixels += pixelPair

        returnValue = True
        if currentMessageIndex != len(messageBinary):
            print(f"Warning: only encoded {len(messageBinary[0:currentMessageIndex])} of {len(messageBinary)} bits ({round(100*len(messageBinary[0:currentMessageIndex])/len(messageBinary), 2)}%)")
            returnValue = False

            # Verbose errors
            if verbose == True:
                # Underline section of binary that was encoded
                # Get max printable width in current terminal
                width = os.get_terminal_size()[0]
                if len(messageBinary) > width * 5:
                    print("Unable to print verbose warning, return binary exceeds maximum length")
                
                # Create array groupings of message lines and underlinings
                printableMessageLines = list(groupImagePixels(messageBinary, width))
                printableUnderlinings = list(groupImagePixels("~"*len(messageBinary[0:currentMessageIndex]), width))

                # Zip and print
                print("\nVerbose warning: only encoded underlined section of message:")
                for printableMessageLine, printableUnderlining in itertools.zip_longest(printableMessageLines, printableUnderlinings, fillvalue=""):
                    print(f"{printableMessageLine}")
                    if printableUnderlining:
                        print(f"{printableUnderlining}")

        # Create new image structure, save file
        newPixels = list(groupImagePixels(newPixels, inFile.size[0]))
        newPixels = pixelArrayToZigZag(newPixels, 1, inFile.size[0], inFile.size[0], inFile.size[1])
        array = np.array(newPixels, dtype=np.uint8)
        savedImage = PIL.Image.fromarray(array)
        savedImage.save(outFile)

        return returnValue
Esempio n. 11
0
    for file in files:
        if imghdr.what(file) in imageTypes:
            imageFiles.append(file)

    # Include directories only if they're readable
    readableDirectories = []
    for directory in directories:
        if os.access(directory, os.R_OK) == True:
            readableDirectories.append(directory)

    # Display neatly
    print(
        f"Supported images in current directory \"{currentWorkingDirectory}\":"
    )
    for imageFile in imageFiles:
        print(imageFile)
    print()

    return False


from PIL.PngImagePlugin import PngImageFile, PngInfo

# Edit PNG metadata to include fingerprint of this PVD algorithm
modifyMetadata = PngImageFile("outCopy.png")
metadata = PngInfo()
metadata.add_text("46", "209")
modifyMetadata.save("outCopy.png", pnginfo=metadata)

testEncoded = PngImageFile("./outCopy.png")
print(testEncoded.text)
from PIL.PngImagePlugin import PngImageFile, PngInfo
import numpy as np
import cv2
#create a dummy image
img = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype="float32")
#png file save as U8 or I16, U16, RGB type
cv2.imwrite("infer.png", img.astype("int8"))
targetImage = PngImageFile("infer.png")
metadata = PngInfo()
metadata.add_text("Rawfloat32", img.newbyteorder().byteswap().tobytes())
targetImage.save("infer.png", pnginfo=metadata)
Esempio n. 13
0
# buffered = io.BytesIO()
# targetImage = PngImageFile("embed-test.png")

# metadata = PngInfo()
# metadata.add_text("MyNewString", "A string")
# metadata.add_text("MyNewInt", str(1234))

# targetImage.save(, pnginfo=metadata)
# imgdata = pybase64.b64decode(buffered.getvalue())
# hostImage = PngImageFile(imgdata)

# # targetImage = PngImageFile("NewPath.png")

# print(hostImage.text)
from PIL.PngImagePlugin import PngImageFile, PngInfo
import json

targetImage = PngImageFile("embed-test.png")

metadata = PngInfo()
metadata.add_text("MyNewString", "A string")
metadata.add_text("MyNewInt", str(1234))

targetImage.save("NewPath.png", pnginfo=metadata)
targetImage = PngImageFile("NewPath.png")
targetImageSize = targetImage.size
print('targetImage', targetImage, 'targetImageSize', targetImageSize)
a = {'name': 'wang', 'age': 29}
print('nameExist', 'name' in a)
b = json.dumps(a)
print('b', b)
Esempio n. 14
0
def insert_instrument_settings(filename="", instrument_dict=[]):
    isimage = False
    if (filename.endswith('PNG') or filename.endswith('png')):
        targetImage = PngImageFile(filename)
        metadata = PngInfo()
        isimage = True
    else:
        outfile = open(filename, 'w')
        settings_dict = {}

    rm = pyvisa.ResourceManager()

    for key in instrument_dict:
        if (key.startswith("HAMEG,HMP4040")):
            hmp4040 = hmp4040_power_supply(
                pyvisa_instr=rm.open_resource(instrument_dict[key]))
            hmp4040_unique_scpi = hmp4040.get_unique_scpi_list()
            if (isimage):
                metadata.add_text("hmp4040_unique_scpi",
                                  json.dumps(hmp4040_unique_scpi))
            else:
                settings_dict['hmp4040_unique_scpi'] = hmp4040_unique_scpi
        if (key.startswith("KEITHLEY INSTRUMENTS INC.,MODEL 2308")):
            k2308 = keithley_2308(
                pyvisa_instr=rm.open_resource(instrument_dict[key]))
            k2308_unique_scpi = k2308.get_unique_scpi_list()
            if (isimage):
                metadata.add_text("k2308_unique_scpi",
                                  json.dumps(k2803_unique_scpi))
            else:
                settings_dict['k2308_unique_scpi'] = k2308_unique_scpi
        if (key.startswith("KEITHLEY INSTRUMENTS INC.,MODEL 2460")):
            k2460 = keithley_2460(
                pyvisa_instr=rm.open_resource(instrument_dict[key]))
            k2460_unique_scpi = k2460.get_unique_scpi_list()
            if (isimage):
                metadata.add_text("k2460_unique_scpi",
                                  json.dumps(k2460_unique_scpi))
            else:
                settings_dict['k2460_unique_scpi'] = k2460_unique_scpi
        if (key.startswith("TEKTRONIX,AFG3102")):
            tek_afg3000 = tektronics_afg3000(
                pyvisa_instr=rm.open_resource(instrument_dict[key]))
            tek_afg3000_unique_scpi = tek_afg3000.get_unique_scpi_list()
            if (isimage):
                metadata.add_text("tek_afg3000_unique_scpi",
                                  json.dumps(tek_afg3000_unique_scpi))
            else:
                settings_dict[
                    'tek_afg3000_unique_scpi'] = tek_afg3000_unique_scpi
        if (key.startswith("KIKUSUI,PLZ164WA,")):
            plz4w = kikusui_plz4w(
                pyvisa_instr=rm.open_resource(instrument_dict[key]))
            plz4w_unique_scpi = plz4w.get_unique_scpi_list()
            if (isimage):
                metadata.add_text("plzw4_unique_scpi",
                                  json.dumps(plz4w_unique_scpi))
            else:
                settings_dict['plz4w_unique_scpi'] = plz4w_unique_scpi
        if (key.startswith("Agilent Technologies,33250A")):
            key_33250a = keysight_33250a(
                pyvisa_instr=rm.open_resource(instrument_dict[key]))
            key_33250a_unique_scpi = key_33250a.get_unique_scpi_list()
            if (isimage):
                metadata.add_text("key_33250a_unique_scpi",
                                  json.dumps(key_33250a_unique_scpi))
            else:
                settings_dict[
                    'key_33250a_unique_scpi'] = key_33250a_unique_scpi
        targetImage.save(filename, pnginfo=metadata)
    if (not isimage):
        json.dump(settings_dict, outfile)
Esempio n. 15
0
def add_to_pnginfo(image_filename="", data_name="", data_dict={}):
    targetImage = PngImageFile(image_filename)
    metadata = PngInfo()
    metadata.add_text(data_name, json.dumps(data_dict))
    targetImage.save(image_filename, pnginfo=metadata)
Esempio n. 16
0
def remove_all_pnginfo(image_filename=""):
    targetImage = PngImageFile(image_filename)
    metadata = ""
    targetImage.save(image_filename, pnginfo=metadata)
Esempio n. 17
0
    def main(self):
        message_broker_host = local.env.get("MESSAGE_BROKER_HOST")
        message_broker_exchange_name = local.env.get(
            "MESSAGE_BROKER_EXCHANGE_NAME", "observed_cam_events")
        rtsp_endpoint = local.env.get("RTSP_ENDPOINT")
        yolo_host = local.env.get("YOLO_HOST")
        yolo_port = local.env.get("YOLO_PORT", 8080)

        # If specified, will save a png of the capture for any detection,
        # with the detections included, json encoded as png metadata.
        image_save_path = local.env.get("IMAGE_SAVE_PATH")
        if image_save_path:
            image_save_path = local.path(image_save_path)
            image_save_path.mkdir()
        threshold = local.env.get("THRESHOLD", "0.25")

        # Include image specifies whether to include a cropped image in the message for a detection
        include_image = local.env.get("INCLUDE_IMAGE")
        if include_image in FALSEY_VALUES:
            include_image = False

        yolo_endpoint = f"http://{yolo_host}:{yolo_port}/detect"

        with broker.MessageBrokerConnection(message_broker_host) as con:
            con.channel.exchange_declare(exchange=message_broker_exchange_name,
                                         exchange_type="fanout")

            cam = Camera(rtsp_endpoint)
            try:
                logging.info("Capturing frames")
                while True:
                    frame = cam.getFrame()
                    dt = datetime.datetime.now(datetime.timezone.utc)
                    if frame is None:
                        # No new frame, sleeping
                        time.sleep(0.1)
                        continue

                    with cam.lock:
                        enc_success, encoded = cv2.imencode(".png", frame)

                    if not enc_success:
                        logging.error("Failed to encode frame")
                        continue

                    response = requests.post(
                        yolo_endpoint,
                        files={"image_file": encoded},
                        data={"threshold": threshold},
                    )

                    try:
                        decoded_response = json.loads(response.text)
                    except Exception as e:
                        logging.exception(
                            f"Failed to parse response response {response.text}"
                        )
                        continue

                    formatted_detections_no_image = []
                    for detection in decoded_response:
                        logging.info(f"got detection: {detection}")
                        body = {
                            "stream": rtsp_endpoint,
                            "time": dt.isoformat(),
                            "detection": detection,
                        }

                        formatted_detections_no_image.append(body.copy())

                        if include_image:
                            xcenter, ycenter, width, height = detection[2]
                            w2 = width / 2
                            h2 = height / 2
                            subframe = frame[max(int(ycenter -
                                                     h2), 0):int(ycenter + h2),
                                             max(int(xcenter -
                                                     w2), 0):int(xcenter +
                                                                 w2), ]

                            enc_success, subframe_encoded = cv2.imencode(
                                ".png", subframe)

                            body["image"] = base64.b64encode(
                                subframe_encoded).decode("ascii")

                        con.channel.basic_publish(
                            exchange=message_broker_exchange_name,
                            routing_key="",
                            body=json.dumps(body),
                        )

                    # Save image to disk with detection metadata if asked for and we had detections
                    if image_save_path and formatted_detections_no_image:
                        pif = PngImageFile(io.BytesIO(encoded))

                        metadata = PngInfo()
                        metadata.add_text(
                            "detections",
                            json.dumps(formatted_detections_no_image))

                        target_folder_name = make_safe_for_filename(
                            rtsp_endpoint)
                        folder = (image_save_path / target_folder_name /
                                  str(dt.year) / str(dt.month) / str(dt.day))
                        folder.mkdir()

                        pif.save(
                            folder /
                            f"{dt.hour}-{dt.minute}-{dt.second}-{dt.microsecond}.png",
                            pnginfo=metadata,
                        )

                    time.sleep(0.5)
            finally:
                cam.close()