open_toontown_panda3d/panda/src/vision/openCVTexture.cxx

630 lines
18 KiB
C++

/**
* PANDA 3D SOFTWARE
* Copyright (c) Carnegie Mellon University. All rights reserved.
*
* All use of this software is subject to the terms of the revised BSD
* license. You should have received a copy of this license along
* with this source code in a file named "LICENSE."
*
* @file openCVTexture.cxx
* @author zacpavlov
* @date 2005-08-19
*/
#include "pandabase.h"
#ifdef HAVE_OPENCV
#include "openCVTexture.h"
#include "clockObject.h"
#include "config_gobj.h"
#include "config_vision.h"
#include "bamReader.h"
#include "bamCacheRecord.h"
// This symbol is predefined by the Panda3D build system to select whether we
// are using the OpenCV 3.x or later interface.
#if defined(OPENCV_VER_3)
#include <opencv2/core.hpp>
#include <opencv2/videoio/videoio_c.h>
// This checks for 2.3 or later.
#elif defined(OPENCV_VER_23)
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
// If neither of those are predefined, assume 1.x.
#else
#include <cv.h>
#include <cxcore.h>
#include <highgui.h>
#endif // OPENCV_VER_3
TypeHandle OpenCVTexture::_type_handle;
/**
* Sets up the texture to read frames from a camera
*/
OpenCVTexture::
OpenCVTexture(const std::string &name) :
VideoTexture(name)
{
}
/**
*
*/
OpenCVTexture::
~OpenCVTexture() {
}
/**
* Calls update_frame() if the current frame has changed.
*/
void OpenCVTexture::
consider_update() {
int this_frame = ClockObject::get_global_clock()->get_frame_count();
if (this_frame != _last_frame_update) {
int frame = get_frame();
if (_current_frame != frame) {
Texture::CDWriter cdata(Texture::_cycler, false);
do_update_frame(cdata, frame);
_current_frame = frame;
} else {
// Loop through the pages to see if there's any camera stream to update.
Texture::CDWriter cdata(Texture::_cycler, false);
int max_z = std::max(cdata->_z_size, (int)_pages.size());
for (int z = 0; z < max_z; ++z) {
VideoPage &page = _pages[z];
if (!page._color.is_from_file() || !page._alpha.is_from_file()) {
do_update_frame(cdata, frame, z);
}
}
}
_last_frame_update = this_frame;
}
}
/**
* Returns a new copy of the same Texture. This copy, if applied to geometry,
* will be copied into texture as a separate texture from the original, so it
* will be duplicated in texture memory (and may be independently modified if
* desired).
*
* If the Texture is an OpenCVTexture, the resulting duplicate may be animated
* independently of the original.
*/
PT(Texture) OpenCVTexture::
make_copy_impl() const {
Texture::CDReader cdata_tex(Texture::_cycler);
PT(OpenCVTexture) copy = new OpenCVTexture(get_name());
Texture::CDWriter cdata_copy_tex(copy->Texture::_cycler, true);
copy->do_assign(cdata_copy_tex, this, cdata_tex);
return copy;
}
/**
* Implements make_copy().
*/
void OpenCVTexture::
do_assign(Texture::CData *cdata_tex, const OpenCVTexture *copy,
const Texture::CData *cdata_copy_tex) {
VideoTexture::do_assign(cdata_tex, copy, cdata_copy_tex);
_pages = copy->_pages;
}
/**
* Sets up the OpenCVTexture (or the indicated page, if z is specified) to
* accept its input from the camera with the given index number, or the
* default camera if the index number is -1 or unspecified.
*
* If alpha_file_channel is 0, then the camera image becomes a normal RGB
* texture. If it is 1, 2, or 3, then the camera image becomes an alpha
* texture, using the indicated channel of the source.
*/
bool OpenCVTexture::
from_camera(int camera_index, int z, int alpha_file_channel,
const LoaderOptions &options) {
Texture::CDWriter cdata(Texture::_cycler, true);
if (!do_reconsider_z_size(cdata, z, options)) {
return false;
}
nassertr(z >= 0 && z < cdata->_z_size, false);
cdata->_alpha_file_channel = alpha_file_channel;
VideoPage &page = do_modify_page(cdata, z);
if (alpha_file_channel == 0) {
// A normal RGB texture.
page._alpha.clear();
if (!page._color.from_camera(camera_index)) {
return false;
}
if (!do_reconsider_video_properties(cdata, page._color, 3, z, options)) {
page._color.clear();
return false;
}
} else {
// An alpha texture.
page._color.clear();
if (!page._alpha.from_camera(camera_index)) {
return false;
}
if (!do_reconsider_video_properties(cdata, page._alpha, 1, z, options)) {
page._alpha.clear();
return false;
}
do_set_format(cdata, F_alpha);
}
cdata->_loaded_from_image = true;
clear_current_frame();
do_update_frame(cdata, 0);
return true;
}
/**
* Returns a reference to the zth VideoPage (level) of the texture. In the
* case of a 2-d texture, there is only one page, level 0; but cube maps and
* 3-d textures have more.
*/
OpenCVTexture::VideoPage &OpenCVTexture::
do_modify_page(const Texture::CData *cdata, int z) {
nassertr(z < cdata->_z_size, _pages[0]);
while (z >= (int)_pages.size()) {
_pages.push_back(VideoPage());
}
return _pages[z];
}
/**
* Resets the internal Texture properties when a new video file is loaded.
* Returns true if the new image is valid, false otherwise.
*/
bool OpenCVTexture::
do_reconsider_video_properties(Texture::CData *cdata,
const OpenCVTexture::VideoStream &stream,
int num_components, int z,
const LoaderOptions &options) {
double frame_rate = 0.0f;
int num_frames = 0;
if (stream.is_from_file()) {
frame_rate = cvGetCaptureProperty(stream._capture, CV_CAP_PROP_FPS);
num_frames = (int)cvGetCaptureProperty(stream._capture, CV_CAP_PROP_FRAME_COUNT);
if (vision_cat.is_debug()) {
vision_cat.debug()
<< "Loaded " << stream._filename << ", " << num_frames << " frames at "
<< frame_rate << " fps\n";
}
} else {
// In this case, we don't have a specific frame rate or number of frames.
// Let both values remain at 0.
if (vision_cat.is_debug()) {
vision_cat.debug()
<< "Loaded camera stream\n";
}
}
int width = (int)cvGetCaptureProperty(stream._capture, CV_CAP_PROP_FRAME_WIDTH);
int height = (int)cvGetCaptureProperty(stream._capture, CV_CAP_PROP_FRAME_HEIGHT);
int x_size = width;
int y_size = height;
do_adjust_this_size(cdata, x_size, y_size, get_name(), true);
if (vision_cat.is_debug()) {
vision_cat.debug()
<< "Video stream is " << width << " by " << height
<< " pixels; fitting in texture " << x_size << " by "
<< y_size << " texels.\n";
}
if (!do_reconsider_image_properties(cdata, x_size, y_size, num_components,
T_unsigned_byte, z, options)) {
return false;
}
if (cdata->_loaded_from_image &&
(get_video_width() != width || get_video_height() != height ||
get_num_frames() != num_frames || get_frame_rate() != frame_rate)) {
vision_cat.error()
<< "Video properties have changed for texture " << get_name()
<< " level " << z << ".\n";
return false;
}
set_frame_rate(frame_rate);
set_num_frames(num_frames);
set_video_size(width, height);
// By default, the newly-loaded video stream will immediately start looping.
loop(true);
return true;
}
/**
* A factory function to make a new OpenCVTexture, used to pass to the
* TexturePool.
*/
PT(Texture) OpenCVTexture::
make_texture() {
return new OpenCVTexture;
}
/**
* Called once per frame, as needed, to load the new image contents.
*/
void OpenCVTexture::
do_update_frame(Texture::CData *cdata, int frame) {
int max_z = std::max(cdata->_z_size, (int)_pages.size());
for (int z = 0; z < max_z; ++z) {
do_update_frame(cdata, frame, z);
}
}
/**
* This variant of update_frame updates the indicated page only.
*/
void OpenCVTexture::
do_update_frame(Texture::CData *cdata, int frame, int z) {
if (vision_cat.is_spam()) {
vision_cat.spam()
<< "Updating OpenCVTexture page " << z << "\n";
}
VideoPage &page = _pages[z];
if (page._color.is_valid() || page._alpha.is_valid()) {
do_modify_ram_image(cdata);
++(cdata->_image_modified);
}
int dest_x_pitch = cdata->_num_components * cdata->_component_width;
int dest_y_pitch = cdata->_x_size * dest_x_pitch;
if (page._color.is_valid()) {
nassertv(get_num_components() >= 3 && get_component_width() == 1);
const unsigned char *r, *g, *b;
int x_pitch, y_pitch;
if (page._color.get_frame_data(frame, r, g, b, x_pitch, y_pitch)) {
nassertv(get_video_width() <= cdata->_x_size && get_video_height() <= cdata->_y_size);
nassertv(!cdata->_ram_images.empty())
unsigned char *dest = cdata->_ram_images[0]._image.p() + do_get_expected_ram_page_size(cdata) * z;
if (cdata->_num_components == 3 && x_pitch == 3) {
// The easy case--copy the whole thing in, row by row.
int copy_bytes = get_video_width() * dest_x_pitch;
nassertv(copy_bytes <= dest_y_pitch && copy_bytes <= abs(y_pitch));
for (int y = 0; y < get_video_height(); ++y) {
memcpy(dest, r, copy_bytes);
dest += dest_y_pitch;
r += y_pitch;
}
} else {
// The harder case--interleave in the color channels, pixel by pixel,
// possibly leaving room for alpha.
for (int y = 0; y < get_video_height(); ++y) {
int dx = 0;
int sx = 0;
for (int x = 0; x < get_video_width(); ++x) {
dest[dx] = r[sx];
dest[dx + 1] = g[sx];
dest[dx + 2] = b[sx];
dx += dest_x_pitch;
sx += x_pitch;
}
dest += dest_y_pitch;
r += y_pitch;
g += y_pitch;
b += y_pitch;
}
}
}
}
if (page._alpha.is_valid()) {
nassertv(get_component_width() == 1);
const unsigned char *source[3];
int x_pitch, y_pitch;
if (page._alpha.get_frame_data(frame, source[0], source[1], source[2],
x_pitch, y_pitch)) {
nassertv(get_video_width() <= cdata->_x_size && get_video_height() <= cdata->_y_size);
nassertv(!cdata->_ram_images.empty())
unsigned char *dest = cdata->_ram_images[0]._image.p() + do_get_expected_ram_page_size(cdata) * z;
// Interleave the alpha in with the color, pixel by pixel. Even though
// the alpha will probably be a grayscale video, the OpenCV library
// presents it as RGB.
const unsigned char *sch = source[0];
if (cdata->_alpha_file_channel >= 1 && cdata->_alpha_file_channel <= 3) {
sch = source[cdata->_alpha_file_channel - 1];
}
for (int y = 0; y < get_video_height(); ++y) {
// Start dx at _num_components - 1, which writes to the last channel,
// i.e. the alpha channel.
int dx = (cdata->_num_components - 1) * cdata->_component_width;
int sx = 0;
for (int x = 0; x < get_video_width(); ++x) {
dest[dx] = sch[sx];
dx += dest_x_pitch;
sx += x_pitch;
}
dest += dest_y_pitch;
sch += y_pitch;
}
}
}
}
/**
* Combines a color and alpha video image from the two indicated filenames.
* Both must be the same kind of video with similar properties.
*/
bool OpenCVTexture::
do_read_one(Texture::CData *cdata,
const Filename &fullpath, const Filename &alpha_fullpath,
int z, int n, int primary_file_num_channels, int alpha_file_channel,
const LoaderOptions &options,
bool header_only, BamCacheRecord *record) {
if (record != nullptr) {
record->add_dependent_file(fullpath);
}
nassertr(n == 0, false);
nassertr(z >= 0 && z < cdata->_z_size, false);
VideoPage &page = do_modify_page(cdata, z);
if (!page._color.read(fullpath)) {
vision_cat.error()
<< "OpenCV couldn't read " << fullpath << " as video.\n";
return false;
}
if (!alpha_fullpath.empty()) {
if (!page._alpha.read(alpha_fullpath)) {
vision_cat.error()
<< "OpenCV couldn't read " << alpha_fullpath << " as video.\n";
page._color.clear();
return false;
}
}
if (z == 0) {
if (!has_name()) {
set_name(fullpath.get_basename_wo_extension());
}
// Don't use has_filename() here, it will cause a deadlock
if (cdata->_filename.empty()) {
cdata->_filename = fullpath;
cdata->_alpha_filename = alpha_fullpath;
}
cdata->_fullpath = fullpath;
cdata->_alpha_fullpath = alpha_fullpath;
}
cdata->_primary_file_num_channels = 3;
cdata->_alpha_file_channel = 0;
if (alpha_fullpath.empty()) {
// Only one RGB movie.
if (!do_reconsider_video_properties(cdata, page._color, 3, z, options)) {
page._color.clear();
return false;
}
} else {
// An RGB movie combined with an alpha movie.
cdata->_alpha_file_channel = alpha_file_channel;
if (!do_reconsider_video_properties(cdata, page._color, 4, z, options)) {
page._color.clear();
page._alpha.clear();
return false;
}
if (!do_reconsider_video_properties(cdata, page._alpha, 4, z, options)) {
page._color.clear();
page._alpha.clear();
return false;
}
}
set_loaded_from_image();
clear_current_frame();
do_update_frame(cdata, 0);
return true;
}
/**
* Resets the texture (or the particular level of the texture) to the
* indicated static image.
*/
bool OpenCVTexture::
do_load_one(Texture::CData *cdata,
const PNMImage &pnmimage, const std::string &name,
int z, int n, const LoaderOptions &options) {
if (z <= (int)_pages.size()) {
VideoPage &page = do_modify_page(cdata, z);
page._color.clear();
page._alpha.clear();
}
return Texture::do_load_one(cdata, pnmimage, name, z, n, options);
}
/**
* Factory method to generate a Texture object
*/
void OpenCVTexture::
register_with_read_factory() {
// Since Texture is such a funny object that is reloaded from the
// TexturePool each time, instead of actually being read fully from the bam
// file, and since the VideoTexture and OpenCVTexture classes don't really
// add any useful data to the bam record, we don't need to define
// make_from_bam(), fillin(), or write_datagram() in this class--we just
// inherit the same functions from Texture.
// We do, however, have to register this class with the BamReader, to avoid
// warnings about creating the wrong kind of object from the bam file.
BamReader::get_factory()->register_factory(get_class_type(), make_from_bam);
}
/**
*
*/
OpenCVTexture::VideoStream::
VideoStream() :
_capture(nullptr),
_camera_index(-1),
_next_frame(0)
{
}
/**
*
*/
OpenCVTexture::VideoStream::
VideoStream(const OpenCVTexture::VideoStream &copy) :
_capture(nullptr),
_camera_index(-1)
{
// Rather than copying the _capture pointer, we must open a new stream that
// references the same file.
if (copy.is_valid()) {
if (copy.is_from_file()) {
read(copy._filename);
} else {
from_camera(copy._camera_index);
}
}
}
/**
*
*/
OpenCVTexture::VideoStream::
~VideoStream() {
clear();
}
/**
* Gets the data needed to traverse through the decompressed buffer for the
* indicated frame number. It is most efficient to call this in increasing
* order of frame number. Returns true on success, false on failure.
*
* In the case of a success indication (true return value), the three pointers
* r, g, b are loaded with the addresses of the three components of the
* bottom-left pixel of the image. (They will be adjacent in memory in the
* case of an interleaved image, and separated in the case of a separate-
* channel image.) The x_pitch value is filled with the amount to add to each
* pointer to advance to the pixel to the right; and the y_pitch value is
* filled with the amount to add to each pointer to advance to the pixel
* above. Note that these values may be negative (particularly in the case of
* a top-down image).
*/
bool OpenCVTexture::VideoStream::
get_frame_data(int frame,
const unsigned char *&r,
const unsigned char *&g,
const unsigned char *&b,
int &x_pitch, int &y_pitch) {
nassertr(is_valid(), false);
if (is_from_file() && _next_frame != frame) {
cvSetCaptureProperty(_capture, CV_CAP_PROP_POS_FRAMES, frame);
}
_next_frame = frame + 1;
IplImage *image = cvQueryFrame(_capture);
if (image == nullptr) {
return false;
}
r = (const unsigned char *)image->imageData;
g = r + 1;
b = g + 1;
x_pitch = 3;
y_pitch = image->widthStep;
if (image->dataOrder == 1) {
// Separate channel images. That means a block of r, followed by a block
// of g, followed by a block of b.
x_pitch = 1;
g = r + image->height * y_pitch;
b = g + image->height * y_pitch;
}
if (image->origin == 0) {
// The image data starts with the top row and ends with the bottom row--
// the opposite of Texture::_ram_data's storage convention. Therefore, we
// must increment the initial pointers to the last row, and count
// backwards.
r += (image->height - 1) * y_pitch;
g += (image->height - 1) * y_pitch;
b += (image->height - 1) * y_pitch;
y_pitch = -y_pitch;
}
return true;
}
/**
* Sets up the stream to read the indicated file. Returns true on success,
* false on failure.
*/
bool OpenCVTexture::VideoStream::
read(const Filename &filename) {
clear();
std::string os_specific = filename.to_os_specific();
_capture = cvCaptureFromFile(os_specific.c_str());
if (_capture == nullptr) {
return false;
}
_filename = filename;
return true;
}
/**
* Sets up the stream to display the indicated camera. Returns true on
* success, false on failure.
*/
bool OpenCVTexture::VideoStream::
from_camera(int camera_index) {
clear();
_capture = cvCaptureFromCAM(camera_index);
if (_capture == nullptr) {
return false;
}
_camera_index = camera_index;
return true;
}
/**
* Stops the video playback and frees the associated resources.
*/
void OpenCVTexture::VideoStream::
clear() {
if (_capture != nullptr) {
cvReleaseCapture(&_capture);
_capture = nullptr;
}
_filename = Filename();
_camera_index = -1;
_next_frame = 0;
}
#endif // HAVE_OPENCV