cleanup code a bit. Move BlobStats to the Zone Object so that it isn't constantly being allocated/freed from the heap. Improve ParsePolygon.
parent
8d7b79bd03
commit
37c829f2bf
147
src/zm_zone.cpp
147
src/zm_zone.cpp
|
@ -113,8 +113,10 @@ void Zone::Setup(
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( config.record_diag_images ) {
|
if ( config.record_diag_images ) {
|
||||||
snprintf(diag_path, sizeof(diag_path), config.record_diag_images_fifo ? "%s/diagpipe-%d-poly.jpg" : "%s/diag-%d-poly.jpg", monitor->getStorage()->Path(), id);
|
snprintf(diag_path, sizeof(diag_path),
|
||||||
if (config.record_diag_images_fifo)
|
config.record_diag_images_fifo ? "%s/diagpipe-%d-poly.jpg" : "%s/diag-%d-poly.jpg",
|
||||||
|
monitor->getStorage()->Path(), id);
|
||||||
|
if ( config.record_diag_images_fifo )
|
||||||
FifoStream::fifo_create_if_missing(diag_path);
|
FifoStream::fifo_create_if_missing(diag_path);
|
||||||
pg_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
pg_image->WriteJpeg(diag_path, config.record_diag_images_fifo);
|
||||||
} else {
|
} else {
|
||||||
|
@ -129,7 +131,7 @@ Zone::~Zone() {
|
||||||
delete[] ranges;
|
delete[] ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Zone::RecordStats( const Event *event ) {
|
void Zone::RecordStats(const Event *event) {
|
||||||
static char sql[ZM_SQL_MED_BUFSIZ];
|
static char sql[ZM_SQL_MED_BUFSIZ];
|
||||||
db_mutex.lock();
|
db_mutex.lock();
|
||||||
snprintf(sql, sizeof(sql),
|
snprintf(sql, sizeof(sql),
|
||||||
|
@ -245,7 +247,7 @@ bool Zone::CheckAlarms(const Image *delta_image) {
|
||||||
|
|
||||||
if ( config.record_diag_images_fifo ) {
|
if ( config.record_diag_images_fifo ) {
|
||||||
FifoDebug(5, "{\"zone\":%d,\"type\":\"ALRM\",\"pixels\":%d,\"avg_diff\":%d}",
|
FifoDebug(5, "{\"zone\":%d,\"type\":\"ALRM\",\"pixels\":%d,\"avg_diff\":%d}",
|
||||||
id,alarm_pixels, pixel_diff);
|
id, alarm_pixels, pixel_diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( alarm_pixels ) {
|
if ( alarm_pixels ) {
|
||||||
|
@ -262,11 +264,7 @@ bool Zone::CheckAlarms(const Image *delta_image) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( max_alarm_pixels != 0 )
|
score = (100*alarm_pixels)/(max_alarm_pixels?max_alarm_pixels:polygon.Area());
|
||||||
score = (100*alarm_pixels)/max_alarm_pixels;
|
|
||||||
else
|
|
||||||
score = (100*alarm_pixels)/polygon.Area();
|
|
||||||
|
|
||||||
if ( score < 1 )
|
if ( score < 1 )
|
||||||
score = 1; /* Fix for score of 0 when frame meets thresholds but alarmed area is not big enough */
|
score = 1; /* Fix for score of 0 when frame meets thresholds but alarmed area is not big enough */
|
||||||
Debug(5, "Current score is %d", score);
|
Debug(5, "Current score is %d", score);
|
||||||
|
@ -356,8 +354,7 @@ bool Zone::CheckAlarms(const Image *delta_image) {
|
||||||
|
|
||||||
if ( check_method >= BLOBS ) {
|
if ( check_method >= BLOBS ) {
|
||||||
Debug(5, "Checking for blob pixels");
|
Debug(5, "Checking for blob pixels");
|
||||||
typedef struct { unsigned char tag; int count; int lo_x; int hi_x; int lo_y; int hi_y; } BlobStats;
|
// ICON FIXME Would like to get rid of this memset
|
||||||
BlobStats blob_stats[256];
|
|
||||||
memset(blob_stats, 0, sizeof(BlobStats)*256);
|
memset(blob_stats, 0, sizeof(BlobStats)*256);
|
||||||
uint8_t *spdiff;
|
uint8_t *spdiff;
|
||||||
uint8_t last_x, last_y;
|
uint8_t last_x, last_y;
|
||||||
|
@ -367,22 +364,15 @@ bool Zone::CheckAlarms(const Image *delta_image) {
|
||||||
int lo_x = ranges[y].lo_x;
|
int lo_x = ranges[y].lo_x;
|
||||||
int hi_x = ranges[y].hi_x;
|
int hi_x = ranges[y].hi_x;
|
||||||
|
|
||||||
pdiff = (uint8_t*)diff_image->Buffer( lo_x, y );
|
pdiff = (uint8_t*)diff_image->Buffer(lo_x, y);
|
||||||
for ( int x = lo_x; x <= hi_x; x++, pdiff++ ) {
|
for ( int x = lo_x; x <= hi_x; x++, pdiff++ ) {
|
||||||
if ( *pdiff == WHITE ) {
|
if ( *pdiff == WHITE ) {
|
||||||
Debug(9, "Got white pixel at %d,%d (%p)", x, y, pdiff);
|
Debug(9, "Got white pixel at %d,%d (%p)", x, y, pdiff);
|
||||||
//last_x = (x>lo_x)?*(pdiff-1):0;
|
|
||||||
//last_y = (y>lo_y&&x>=last_lo_x&&x<=last_hi_x)?*(pdiff-diff_width):0;
|
|
||||||
|
|
||||||
last_x = 0;
|
last_x = ((x > 0) && ( (x-1) >= lo_x )) ? *(pdiff-1) : 0;
|
||||||
if ( x > 0 ) {
|
|
||||||
if ( (x-1) >= lo_x ) {
|
|
||||||
last_x = *(pdiff-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last_y = 0;
|
last_y = 0;
|
||||||
if (y > 0 ) {
|
if ( y > 0 ) {
|
||||||
if ( (y-1) >= lo_y && ranges[(y-1)].lo_x <= x && ranges[(y-1)].hi_x >= x ) {
|
if ( (y-1) >= lo_y && ranges[(y-1)].lo_x <= x && ranges[(y-1)].hi_x >= x ) {
|
||||||
last_y = *(pdiff-diff_width);
|
last_y = *(pdiff-diff_width);
|
||||||
}
|
}
|
||||||
|
@ -629,7 +619,7 @@ bool Zone::CheckAlarms(const Image *delta_image) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( max_blob_pixels != 0 )
|
if ( max_blob_pixels != 0 )
|
||||||
score = (100*alarm_blob_pixels)/(max_blob_pixels);
|
score = (100*alarm_blob_pixels)/max_blob_pixels;
|
||||||
else
|
else
|
||||||
score = (100*alarm_blob_pixels)/polygon.Area();
|
score = (100*alarm_blob_pixels)/polygon.Area();
|
||||||
|
|
||||||
|
@ -756,67 +746,42 @@ bool Zone::CheckAlarms(const Image *delta_image) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon) {
|
bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon) {
|
||||||
Debug(3, "Parsing polygon string '%s'", poly_string);
|
|
||||||
|
|
||||||
char *str_ptr = new char[strlen(poly_string)+1];
|
|
||||||
char *str = str_ptr;
|
|
||||||
strcpy(str, poly_string);
|
|
||||||
|
|
||||||
|
char *str = (char *)poly_string;
|
||||||
char *ws;
|
char *ws;
|
||||||
|
char *cp;
|
||||||
int n_coords = 0;
|
int n_coords = 0;
|
||||||
int max_n_coords = strlen(str)/4;
|
int max_n_coords = strlen(str)/4;
|
||||||
Coord *coords = new Coord[max_n_coords];
|
Coord *coords = new Coord[max_n_coords];
|
||||||
while( true ) {
|
while ( *str != '\0' ) {
|
||||||
if ( *str == '\0' ) {
|
cp = strchr(str, ',');
|
||||||
break;
|
|
||||||
}
|
|
||||||
ws = strchr(str, ' ');
|
|
||||||
if ( ws ) {
|
|
||||||
*ws = '\0';
|
|
||||||
}
|
|
||||||
char *cp = strchr(str, ',');
|
|
||||||
if ( !cp ) {
|
if ( !cp ) {
|
||||||
Error("Bogus coordinate %s found in polygon string", str);
|
Error("Bogus coordinate %s found in polygon string", str);
|
||||||
delete[] coords;
|
break;
|
||||||
delete[] str_ptr;
|
}
|
||||||
return false;
|
int x = atoi(str);
|
||||||
} else {
|
int y = atoi(cp+1);
|
||||||
*cp = '\0';
|
Debug(3, "Got coordinate %d,%d from polygon string", x, y);
|
||||||
char *xp = str;
|
coords[n_coords++] = Coord(x, y);
|
||||||
char *yp = cp+1;
|
|
||||||
|
|
||||||
int x = atoi(xp);
|
ws = strchr(cp+2, ' ');
|
||||||
int y = atoi(yp);
|
|
||||||
|
|
||||||
Debug(3, "Got coordinate %d,%d from polygon string", x, y);
|
|
||||||
#if 0
|
|
||||||
if ( x < 0 )
|
|
||||||
x = 0;
|
|
||||||
else if ( x >= width )
|
|
||||||
x = width-1;
|
|
||||||
if ( y < 0 )
|
|
||||||
y = 0;
|
|
||||||
else if ( y >= height )
|
|
||||||
y = height-1;
|
|
||||||
#endif
|
|
||||||
coords[n_coords++] = Coord( x, y );
|
|
||||||
}
|
|
||||||
if ( ws )
|
if ( ws )
|
||||||
str = ws+1;
|
str = ws+1;
|
||||||
else
|
else
|
||||||
break;
|
break;
|
||||||
}
|
} // end while ! end of string
|
||||||
polygon = Polygon(n_coords, coords);
|
|
||||||
|
|
||||||
Debug(3, "Successfully parsed polygon string");
|
if ( n_coords > 2 ) {
|
||||||
//printf( "Area: %d\n", pg.Area() );
|
Debug(3, "Successfully parsed polygon string %s", str);
|
||||||
//printf( "Centre: %d,%d\n", pg.Centre().X(), pg.Centre().Y() );
|
polygon = Polygon(n_coords, coords);
|
||||||
|
} else {
|
||||||
|
Error("Not enough coordinates to form a polygon!");
|
||||||
|
n_coords = 0;
|
||||||
|
}
|
||||||
|
|
||||||
delete[] coords;
|
delete[] coords;
|
||||||
delete[] str_ptr;
|
return n_coords ? true : false;
|
||||||
|
} // end bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon)
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, Polygon &polygon) {
|
bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, Polygon &polygon) {
|
||||||
Debug(3, "Parsing zone string '%s'", zone_string);
|
Debug(3, "Parsing zone string '%s'", zone_string);
|
||||||
|
@ -825,13 +790,12 @@ bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, P
|
||||||
char *str = str_ptr;
|
char *str = str_ptr;
|
||||||
strcpy(str, zone_string);
|
strcpy(str, zone_string);
|
||||||
|
|
||||||
|
zone_id = strtol(str, 0, 10);
|
||||||
|
Debug(3, "Got zone %d from zone string", zone_id);
|
||||||
|
|
||||||
char *ws = strchr(str, ' ');
|
char *ws = strchr(str, ' ');
|
||||||
if ( !ws ) {
|
if ( !ws ) {
|
||||||
Debug(3, "No initial whitespace found in zone string '%s', finishing", str);
|
Debug(3, "No initial whitespace found in zone string '%s', finishing", str);
|
||||||
}
|
|
||||||
zone_id = strtol(str, 0, 10);
|
|
||||||
Debug(3, "Got zone %d from zone string", zone_id);
|
|
||||||
if ( !ws ) {
|
|
||||||
delete[] str_ptr;
|
delete[] str_ptr;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -839,13 +803,11 @@ bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, P
|
||||||
*ws = '\0';
|
*ws = '\0';
|
||||||
str = ws+1;
|
str = ws+1;
|
||||||
|
|
||||||
|
colour = strtol(str, 0, 16);
|
||||||
|
Debug(3, "Got colour %06x from zone string", colour);
|
||||||
ws = strchr(str, ' ');
|
ws = strchr(str, ' ');
|
||||||
if ( !ws ) {
|
if ( !ws ) {
|
||||||
Debug(3, "No secondary whitespace found in zone string '%s', finishing", zone_string);
|
Debug(3, "No secondary whitespace found in zone string '%s', finishing", zone_string);
|
||||||
}
|
|
||||||
colour = strtol(str, 0, 16);
|
|
||||||
Debug(3, "Got colour %06x from zone string", colour);
|
|
||||||
if ( !ws ) {
|
|
||||||
delete[] str_ptr;
|
delete[] str_ptr;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -853,27 +815,28 @@ bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, P
|
||||||
str = ws+1;
|
str = ws+1;
|
||||||
|
|
||||||
bool result = ParsePolygonString(str, polygon);
|
bool result = ParsePolygonString(str, polygon);
|
||||||
|
|
||||||
//printf( "Area: %d\n", pg.Area() );
|
|
||||||
//printf( "Centre: %d,%d\n", pg.Centre().X(), pg.Centre().Y() );
|
|
||||||
|
|
||||||
delete[] str_ptr;
|
delete[] str_ptr;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
} // end bool Zone::ParseZoneString(const char *zone_string, int &zone_id, int &colour, Polygon &polygon)
|
||||||
|
|
||||||
int Zone::Load(Monitor *monitor, Zone **&zones) {
|
int Zone::Load(Monitor *monitor, Zone **&zones) {
|
||||||
static char sql[ZM_SQL_MED_BUFSIZ];
|
static char sql[ZM_SQL_MED_BUFSIZ];
|
||||||
|
|
||||||
db_mutex.lock();
|
db_mutex.lock();
|
||||||
snprintf(sql, sizeof(sql), "select Id,Name,Type+0,Units,Coords,AlarmRGB,CheckMethod+0,MinPixelThreshold,MaxPixelThreshold,MinAlarmPixels,MaxAlarmPixels,FilterX,FilterY,MinFilterPixels,MaxFilterPixels,MinBlobPixels,MaxBlobPixels,MinBlobs,MaxBlobs,OverloadFrames,ExtendAlarmFrames from Zones where MonitorId = %d order by Type, Id", monitor->Id());
|
snprintf(sql, sizeof(sql), "SELECT Id,Name,Type+0,Units,Coords,AlarmRGB,CheckMethod+0,"
|
||||||
|
"MinPixelThreshold,MaxPixelThreshold,MinAlarmPixels,MaxAlarmPixels,"
|
||||||
|
"FilterX,FilterY,MinFilterPixels,MaxFilterPixels,"
|
||||||
|
"MinBlobPixels,MaxBlobPixels,MinBlobs,MaxBlobs,"
|
||||||
|
"OverloadFrames,ExtendAlarmFrames"
|
||||||
|
" FROM Zones WHERE MonitorId = %d ORDER BY Type, Id", monitor->Id());
|
||||||
if ( mysql_query(&dbconn, sql) ) {
|
if ( mysql_query(&dbconn, sql) ) {
|
||||||
Error("Can't run query: %s", mysql_error(&dbconn));
|
Error("Can't run query: %s", mysql_error(&dbconn));
|
||||||
db_mutex.unlock();
|
db_mutex.unlock();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MYSQL_RES *result = mysql_store_result( &dbconn );
|
MYSQL_RES *result = mysql_store_result(&dbconn);
|
||||||
if ( !result ) {
|
if ( !result ) {
|
||||||
Error("Can't use query result: %s", mysql_error(&dbconn));
|
Error("Can't use query result: %s", mysql_error(&dbconn));
|
||||||
db_mutex.unlock();
|
db_mutex.unlock();
|
||||||
|
@ -942,7 +905,13 @@ int Zone::Load(Monitor *monitor, Zone **&zones) {
|
||||||
} else if ( atoi(dbrow[2]) == Zone::PRIVACY ) {
|
} else if ( atoi(dbrow[2]) == Zone::PRIVACY ) {
|
||||||
zones[i] = new Zone(monitor, Id, Name, (Zone::ZoneType)Type, polygon);
|
zones[i] = new Zone(monitor, Id, Name, (Zone::ZoneType)Type, polygon);
|
||||||
}
|
}
|
||||||
zones[i] = new Zone(monitor, Id, Name, (Zone::ZoneType)Type, polygon, AlarmRGB, (Zone::CheckMethod)CheckMethod, MinPixelThreshold, MaxPixelThreshold, MinAlarmPixels, MaxAlarmPixels, Coord( FilterX, FilterY ), MinFilterPixels, MaxFilterPixels, MinBlobPixels, MaxBlobPixels, MinBlobs, MaxBlobs, OverloadFrames, ExtendAlarmFrames);
|
zones[i] = new Zone(
|
||||||
|
monitor, Id, Name, (Zone::ZoneType)Type, polygon, AlarmRGB,
|
||||||
|
(Zone::CheckMethod)CheckMethod, MinPixelThreshold, MaxPixelThreshold,
|
||||||
|
MinAlarmPixels, MaxAlarmPixels, Coord( FilterX, FilterY ),
|
||||||
|
MinFilterPixels, MaxFilterPixels,
|
||||||
|
MinBlobPixels, MaxBlobPixels, MinBlobs, MaxBlobs,
|
||||||
|
OverloadFrames, ExtendAlarmFrames);
|
||||||
} // end foreach row
|
} // end foreach row
|
||||||
mysql_free_result(result);
|
mysql_free_result(result);
|
||||||
return n_zones;
|
return n_zones;
|
||||||
|
@ -951,9 +920,9 @@ int Zone::Load(Monitor *monitor, Zone **&zones) {
|
||||||
bool Zone::DumpSettings(char *output, bool /*verbose*/) {
|
bool Zone::DumpSettings(char *output, bool /*verbose*/) {
|
||||||
output[0] = 0;
|
output[0] = 0;
|
||||||
|
|
||||||
sprintf( output+strlen(output), " Id : %d\n", id );
|
sprintf(output+strlen(output), " Id : %d\n", id );
|
||||||
sprintf( output+strlen(output), " Label : %s\n", label );
|
sprintf(output+strlen(output), " Label : %s\n", label );
|
||||||
sprintf( output+strlen(output), " Type: %d - %s\n", type,
|
sprintf(output+strlen(output), " Type: %d - %s\n", type,
|
||||||
type==ACTIVE?"Active":(
|
type==ACTIVE?"Active":(
|
||||||
type==INCLUSIVE?"Inclusive":(
|
type==INCLUSIVE?"Inclusive":(
|
||||||
type==EXCLUSIVE?"Exclusive":(
|
type==EXCLUSIVE?"Exclusive":(
|
||||||
|
@ -1014,10 +983,10 @@ void Zone::std_alarmedpixels(Image* pdiff_image, const Image* ppoly_image, unsig
|
||||||
*pdiff = BLACK;
|
*pdiff = BLACK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} // end for y = lo_y to hi_y
|
||||||
|
|
||||||
/* Store the results */
|
/* Store the results */
|
||||||
*pixel_count = pixelsalarmed;
|
*pixel_count = pixelsalarmed;
|
||||||
*pixel_sum = pixelsdifference;
|
*pixel_sum = pixelsdifference;
|
||||||
Debug(7, "STORED pixelsalarmed(%d), pixelsdifference(%d)", pixelsalarmed, pixelsdifference);
|
Debug(7, "STORED pixelsalarmed(%d), pixelsdifference(%d)", pixelsalarmed, pixelsdifference);
|
||||||
}
|
} // end void Zone::std_alarmedpixels(Image* pdiff_image, const Image* ppoly_image, unsigned int* pixel_count, unsigned int* pixel_sum)
|
||||||
|
|
|
@ -32,15 +32,14 @@ class Monitor;
|
||||||
// This describes a 'zone', or an area of an image that has certain
|
// This describes a 'zone', or an area of an image that has certain
|
||||||
// detection characteristics.
|
// detection characteristics.
|
||||||
//
|
//
|
||||||
class Zone
|
class Zone {
|
||||||
{
|
|
||||||
protected:
|
protected:
|
||||||
struct Range
|
struct Range {
|
||||||
{
|
|
||||||
int lo_x;
|
int lo_x;
|
||||||
int hi_x;
|
int hi_x;
|
||||||
int off_x;
|
int off_x;
|
||||||
};
|
};
|
||||||
|
typedef struct { unsigned char tag; int count; int lo_x; int hi_x; int lo_y; int hi_y; } BlobStats;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
typedef enum { ACTIVE=1, INCLUSIVE, EXCLUSIVE, PRECLUSIVE, INACTIVE, PRIVACY } ZoneType;
|
typedef enum { ACTIVE=1, INCLUSIVE, EXCLUSIVE, PRECLUSIVE, INACTIVE, PRIVACY } ZoneType;
|
||||||
|
@ -52,8 +51,8 @@ protected:
|
||||||
|
|
||||||
int id;
|
int id;
|
||||||
char *label;
|
char *label;
|
||||||
ZoneType type;
|
ZoneType type;
|
||||||
Polygon polygon;
|
Polygon polygon;
|
||||||
Rgb alarm_rgb;
|
Rgb alarm_rgb;
|
||||||
CheckMethod check_method;
|
CheckMethod check_method;
|
||||||
|
|
||||||
|
@ -67,17 +66,18 @@ protected:
|
||||||
int min_filter_pixels;
|
int min_filter_pixels;
|
||||||
int max_filter_pixels;
|
int max_filter_pixels;
|
||||||
|
|
||||||
|
BlobStats blob_stats[256];
|
||||||
int min_blob_pixels;
|
int min_blob_pixels;
|
||||||
int max_blob_pixels;
|
int max_blob_pixels;
|
||||||
int min_blobs;
|
int min_blobs;
|
||||||
int max_blobs;
|
int max_blobs;
|
||||||
|
|
||||||
int overload_frames;
|
int overload_frames;
|
||||||
int extend_alarm_frames;
|
int extend_alarm_frames;
|
||||||
|
|
||||||
// Outputs/Statistics
|
// Outputs/Statistics
|
||||||
bool alarmed;
|
bool alarmed;
|
||||||
bool was_alarmed;
|
bool was_alarmed;
|
||||||
int pixel_diff;
|
int pixel_diff;
|
||||||
unsigned int alarm_pixels;
|
unsigned int alarm_pixels;
|
||||||
int alarm_filter_pixels;
|
int alarm_filter_pixels;
|
||||||
|
@ -139,8 +139,7 @@ public:
|
||||||
inline Coord GetAlarmCentre() const { return( alarm_centre ); }
|
inline Coord GetAlarmCentre() const { return( alarm_centre ); }
|
||||||
inline unsigned int Score() const { return( score ); }
|
inline unsigned int Score() const { return( score ); }
|
||||||
|
|
||||||
inline void ResetStats()
|
inline void ResetStats() {
|
||||||
{
|
|
||||||
alarmed = false;
|
alarmed = false;
|
||||||
was_alarmed = false;
|
was_alarmed = false;
|
||||||
pixel_diff = 0;
|
pixel_diff = 0;
|
||||||
|
|
Loading…
Reference in New Issue