I’ve been struggling with some problems in implementing steganographic methods for video for a really long time.
I am trying to implement a simple LSB coder and decoder in YUV components using Hamming code (7,4) for error correction.
My current code can only decode the secret message from video frames stored on disk (but not from those extracted from reconstructed stego-video). Therefore, I think that there will be a problem somewhere in the conversion to video and back to video frames, it changes the pixels of the images only a little bit, but it is fatal for LSB. Because Hamming code corrects only 1-bit errors.
Second, I study the DWT domain method too, which extracts video frames, splits each RGB frame into YUV, and splits the YUV components using DWT to coefficients. I embed the message using LSB in LH, HL and HH coefficients. I use BCH codes (15,11) to correct errors ( I tried (15,7) and (15, 5) too but there are more than 3 error bits per codeword, i am using galois library for bCh codes). Here there is a problem when saving the stego-frame to disk and then decoding it (without reconstruction to video), many bit errors occur when reading the message from the DWT coefficient. This causes a problem especially when I want to use an image or other file converted to a bit array as a message. Is it possible that I am working with the DWT domain in a wrong way?
Otherwise, this part also has the same problem as above: the message is not readable after extracting the stego-frames containing the message from the stego-video.
So my questions are:
- How to process a video and frames to solve my problem with unreadable message after reconstruction of stego-video?
- How to solve a problem with so much errors while extracting codewords from DWT coefficients of one stego-frame? I am doing it right?
I was trying hard to find solution all over the internet,from books to understand all these concepts, to understand this problem more and solve it. But unsuccessfully. I am writing this code in python, using opencv, pywavelets and galois for BCH codes.
Here my functions for processing video to frames and back (where i think will be a problem):
(I also tried process video with cv2.VideoWriter but it was not better)
def video_to_rgb_frames(video_path):
"""Extracts frames from a video file and saves them as individual image files into "/frames" folder. Save as .png files."""
if not os.path.exists("./frames"):
os.makedirs("frames")
temporal_folder="./frames"
print("[INFO] frames directory is created")
capture = cv2.VideoCapture(video_path)
if not capture.isOpened():
print("Error: Video file cannot be opened!")
return
video_properties = {
"format": capture.get(cv2.CAP_PROP_FORMAT),
"codec": capture.get(cv2.CAP_PROP_FOURCC),
"container": capture.get(cv2.CAP_PROP_POS_AVI_RATIO),
"fps": capture.get(cv2.CAP_PROP_FPS),
"frames": capture.get(cv2.CAP_PROP_FRAME_COUNT),
"width": int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
"height": int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)),
}
frame_count = 0
while True:
ret, frame = capture.read()
if frame is None:
break
frame_count += 1
cv2.imwrite(os.path.join(temporal_folder, f"frame_{frame_count}.png"), frame)
capture.release()
print("[INFO] extraction finished")
return video_properties
def reconstruct_video_from_rgb_frames(file_path, properties, ffmpeg_path = r".srcutilsffmpeg.exe"):
"""Reconstruct video from RGB frames with ffmpeg."""
fps = properties["fps"]
codec = decode_fourcc(properties["codec"])
#file_extension = file_path.rsplit(".", 1)[1]
file_extension = "avi"
bitrate = get_vid_stream_bitrate(file_path)
if has_audio_track(file_path):
#extract audio stream from video
extract_audio_track(file_path)
#recreate video from frames (without audio)
call([ffmpeg_path, "-r", str(fps), "-i", "frames/frame_%d.png" , "-vcodec", str(codec), "-b", str(bitrate),"-crf", "0","-pix_fmt", "yuv420p", f"tmp/video.{file_extension}", "-y"])
#add audio to a recreated video
call([ffmpeg_path, "-i", f"tmp/video.{file_extension}", "-i", "tmp/audio.wav","-q:v", "1", "-codec", "copy", f"video.{file_extension}", "-y"])
else:
#recreate video from frames (without audio)
call([ffmpeg_path, "-r", str(fps), "-i", "frames/frame_%d.png","-q:v", "1", "-vcodec", str(codec), "-b", str(bitrate), "-pix_fmt", "yuv420p", f"video.{file_extension}", "-y"])
print("[INFO] reconstruction is finished")
I am using these two to convert RGB to YUV and back:
def rgb2yuv(image_name):
frame_path = os.path.join(rgb_folder, image_name)
rgb_image = cv2.imread(frame_path)
if rgb_image is None:
print(f"Error: Unable to load {frame_path}")
return
yuv_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2YCrCb)
#yuv_image = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2YUV)
# Split YUV channels
Y = yuv_image[:, :, 0]
U = yuv_image[:, :, 1]
V = yuv_image[:, :, 2]
y_path = os.path.join(y_folder, image_name)
u_path = os.path.join(u_folder, image_name)
v_path = os.path.join(v_folder, image_name)
cv2.imwrite(y_path, Y)
cv2.imwrite(u_path, U)
cv2.imwrite(v_path, V)
def yuv2rgb(image_name):
y_path = os.path.join(y_folder, image_name)
u_path = os.path.join(u_folder, image_name)
v_path = os.path.join(v_folder, image_name)
Y = cv2.imread(y_path, cv2.IMREAD_GRAYSCALE)
U = cv2.imread(u_path, cv2.IMREAD_GRAYSCALE)
V = cv2.imread(v_path, cv2.IMREAD_GRAYSCALE)
if Y is None or U is None or V is None:
print(f"Error: Unable to load components for {image_name}")
return
yuv_image = np.stack((Y, U, V), axis=-1)
rgb_image = cv2.cvtColor(yuv_image, cv2.COLOR_YCrCb2RGB)
#rgb_image = cv2.cvtColor(yuv_image, cv2.COLOR_YUV2RGB)
rgb_path = os.path.join(rgb_folder, image_name)
cv2.imwrite(rgb_path, rgb_image)
And here are parts of my code where I am working with DWT coefficients values. I am using parts 1 – 4 for extracting codewords too:
#part 1)
y_component_path = f"./tmp/Y/frame_{curr_frame}.png"
u_component_path = f"./tmp/U/frame_{curr_frame}.png"
v_component_path = f"./tmp/V/frame_{curr_frame}.png"
y_frame = cv2.imread(y_component_path, cv2.IMREAD_GRAYSCALE)
u_frame = cv2.imread(u_component_path, cv2.IMREAD_GRAYSCALE)
v_frame = cv2.imread(v_component_path, cv2.IMREAD_GRAYSCALE)
#part 2) Apply 2D-DWT to U, V, and Y components
coeffs_Y = pywt.dwt2(y_frame, 'haar')
coeffs_U = pywt.dwt2(u_frame, 'haar')
coeffs_V = pywt.dwt2(v_frame, 'haar')
#part 3) Extract HH, HL, LH subbands
LL_Y, (LH_Y, HL_Y, HH_Y) = coeffs_Y
LL_U, (LH_U, HL_U, HH_U) = coeffs_U
LL_V, (LH_V, HL_V, HH_V) = coeffs_V
#part 4) reading binary values of coefficients
LH_Y_bin = format(floor(abs(LH_Y[row, col] + 0.0000001)), '#010b')
HL_Y_bin = format(floor(abs(HL_Y[row, col] + 0.0000001 )), '#010b')
HH_Y_bin = format(floor(abs(HH_Y[row, col] + 0.0000001)), '#010b')
LH_U_bin = format(floor(abs(LH_U[row, col] + 0.0000001)), '#010b')
HL_U_bin = format(floor(abs(HL_U[row, col] + 0.0000001)), '#010b')
HH_U_bin = format(floor(abs(HH_U[row, col] + 0.0000001)), '#010b')
LH_V_bin = format(floor(abs(LH_V[row, col] + 0.0000001)), '#010b')
HL_V_bin = format(floor(abs(HL_V[row, col] + 0.0000001)), '#010b')
HH_V_bin = format(floor(abs(HH_V[row, col] + 0.0000001)), '#010b')
#part 5) embedding one codeword created by BCH code
LH_Y[row, col] = int(LH_Y_bin[:-3] + ''.join(str(bit) for bit in codeword[:3]), 2)
HL_Y[row, col] = int(HL_Y_bin[:-3] + ''.join(str(bit) for bit in codeword[3:6]), 2)
HH_Y[row, col] = int(HH_Y_bin[:-3] + ''.join(str(bit) for bit in codeword[6:9]), 2)
LH_U[row, col] = int(LH_U_bin[:-1] + str(codeword[9]), 2)
HL_U[row, col] = int(HL_U_bin[:-1] + str(codeword[10]), 2)
HH_U[row, col] = int(HH_U_bin[:-1] + str(codeword[11]), 2)
LH_V[row, col] = int(LH_V_bin[:-1] + str(codeword[12]), 2)
HL_V[row, col] = int(HL_V_bin[:-1] + str(codeword[13]), 2)
HH_V[row, col] = int(HH_V_bin[:-1] + str(codeword[14]), 2)
# part 6) saving current frame
HH_Y = HH_Y.reshape(LL_Y.shape)
HL_Y = HL_Y.reshape(LL_Y.shape)
LH_Y = LH_Y.reshape(LL_Y.shape)
HH_U = HH_U.reshape(LL_U.shape)
HL_U = HL_U.reshape(LL_U.shape)
LH_U = LH_U.reshape(LL_U.shape)
HH_V = HH_V.reshape(LL_V.shape)
HL_V = HL_V.reshape(LL_V.shape)
LH_V = LH_V.reshape(LL_V.shape)
#part 7) Reconstruct frames using inverse DWT
Y_new = pywt.idwt2((LL_Y, (LH_Y, HL_Y, HH_Y)), 'haar')
U_new = pywt.idwt2((LL_U, (LH_U, HL_U, HH_U)), 'haar')
V_new = pywt.idwt2((LL_V, (LH_V, HL_V, HH_V)), 'haar')
cv2.imwrite(y_component_path, Y_new)
cv2.imwrite(u_component_path, U_new)
cv2.imwrite(v_component_path, V_new)
#recreating video with functions above
user25603823 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.